diff --git a/docs/tutorial/exceptions.md b/docs/tutorial/exceptions.md
index 18c3374ff3..5786e59258 100644
--- a/docs/tutorial/exceptions.md
+++ b/docs/tutorial/exceptions.md
@@ -192,6 +192,134 @@ $ python main.py
+## Disable Tracebacks From Certain Modules
+
+If you are developing with Python frameworks other than **Typer**, you might get
+long, verbose tracebacks through other people's code. This could make it
+annoying to find the line in your own code that triggered the exception. And
+seeing internal Python code from someone else's package is almost never helpful
+when you're trying to troubleshoot your own code.
+
+With pretty exceptions, you can use the parameter `pretty_exceptions_suppress`,
+which takes a list of Python modules, or `str` paths. This indicates the modules
+for which the **Rich** traceback formatter should suppress the traceback
+details. Only filename and line number will be shown for these modules, but no
+code or variables.
+
+For example, calling `urllib.request.urlopen()` with an unknown URL protocol will
+produce an exception with many internal stack frames from `urllib.request` that
+you probably don't care about. It could look like this:
+
+
+
+```console
+$ python main.py
+
+╭──────────────── Traceback (most recent call last) ────────────────╮
+│ /home/user/code/superapp/main.py:10 in main │
+│ │
+│ 7 │
+│ 8 @app.command() │
+│ 9 def main(): │
+│ ❱ 10 │ urllib.request.urlopen("unknown://example.com") │
+│ 11 │
+│ 12 │
+│ 13 if __name__ == "__main__": │
+│ │
+│ .../python3.10/urllib/request.py:216 in urlopen │
+│ │
+│ 213 │ │ _opener = opener = build_opener() │
+│ 214 │ else: │
+│ 215 │ │ opener = _opener │
+│ ❱ 216 │ return opener.open(url, data, timeout) │
+│ 217 │
+│ 218 def install_opener(opener): │
+│ 219 │ global _opener │
+│ │
+│ .../python3.10/urllib/request.py:519 in open │
+│ │
+│ 516 │ │ │ req = meth(req) │
+│ 517 │ │ │
+│ 518 │ │ sys.audit('urllib.Request', req.full_url, req.data │
+│ ❱ 519 │ │ response = self._open(req, data) │
+│ 520 │ │ │
+│ 521 │ │ # post-process response │
+│ 522 │ │ meth_name = protocol+"_response" │
+│ │
+│ .../python3.10/urllib/request.py:536 in _open │
+│ │
+│ 533 │ │ │ return result │
+│ 534 │ │ │
+│ 535 │ │ protocol = req.type │
+│ ❱ 536 │ │ result = self._call_chain(self.handle_open, protoc │
+│ 537 │ │ │ │ │ │ │ │ '_open', req) │
+│ 538 │ │ if result: │
+│ 539 │ │ │ return result │
+│ │
+│ .../python3.10/urllib/request.py:496 in _call_chain │
+│ │
+│ 493 │ │ handlers = chain.get(kind, ()) │
+│ 494 │ │ for handler in handlers: │
+│ 495 │ │ │ func = getattr(handler, meth_name) │
+│ ❱ 496 │ │ │ result = func(*args) │
+│ 497 │ │ │ if result is not None: │
+│ 498 │ │ │ │ return result │
+│ 499 │
+│ │
+│ .../python3.10/urllib/request.py:1419 in unknown_open │
+│ │
+│ 1416 class UnknownHandler(BaseHandler): │
+│ 1417 │ def unknown_open(self, req): │
+│ 1418 │ │ type = req.type │
+│ ❱ 1419 │ │ raise URLError('unknown url type: %s' % type) │
+│ 1420 │
+│ 1421 def parse_keqv_list(l): │
+│ 1422 │ """Parse list of key=value strings where keys are not │
+╰───────────────────────────────────────────────────────────────────╯
+URLError: <urlopen error unknown url type: unknown>
+```
+
+
+
+That's a lot of clutter from `urllib.request` internals! You can suppress those
+parts of the traceback with `pretty_exceptions_suppress`:
+
+{* docs_src/exceptions/tutorial005_py310.py hl[5] *}
+
+And now the traceback only shows your own code with full detail, while the
+`urllib.request` frames are reduced to just a filename and line number:
+
+
+
+```console
+$ python main.py
+
+╭─────────────── Traceback (most recent call last) ─────────────────╮
+│ /home/user/code/superapp/main.py:10 in main │
+│ │
+│ 7 │
+│ 8 @app.command() │
+│ 9 def main(): │
+│ ❱ 10 │ urllib.request.urlopen("unknown://example.com") │
+│ 11 │
+│ 12 │
+│ 13 if __name__ == "__main__": │
+│ │
+│ .../python3.10/urllib/request.py:216 in urlopen │
+│ │
+│ .../python3.10/urllib/request.py:519 in open │
+│ │
+│ .../python3.10/urllib/request.py:536 in _open │
+│ │
+│ .../python3.10/urllib/request.py:496 in _call_chain │
+│ │
+│ .../python3.10/urllib/request.py:1419 in unknown_open │
+╰───────────────────────────────────────────────────────────────────╯
+URLError: <urlopen error unknown url type: unknown>
+```
+
+
+
## Disable Pretty Exceptions
You can also entirely disable pretty exceptions with the parameter `pretty_exceptions_enable=False`:
diff --git a/docs_src/exceptions/tutorial005_py310.py b/docs_src/exceptions/tutorial005_py310.py
new file mode 100644
index 0000000000..d3981ad3c8
--- /dev/null
+++ b/docs_src/exceptions/tutorial005_py310.py
@@ -0,0 +1,14 @@
+import urllib.request
+
+import typer
+
+app = typer.Typer(pretty_exceptions_suppress=[urllib.request])
+
+
+@app.command()
+def main():
+ urllib.request.urlopen("unknown://example.com")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/tests/test_tutorial/test_exceptions/test_tutorial005.py b/tests/test_tutorial/test_exceptions/test_tutorial005.py
new file mode 100644
index 0000000000..6899c1e58f
--- /dev/null
+++ b/tests/test_tutorial/test_exceptions/test_tutorial005.py
@@ -0,0 +1,41 @@
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+from typer.testing import CliRunner
+
+from docs_src.exceptions import tutorial005_py310 as mod
+
+runner = CliRunner()
+
+
+def test_pretty_exceptions_suppress():
+ file_path = Path(mod.__file__)
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", str(file_path)],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "TYPER_STANDARD_TRACEBACK": "",
+ "_TYPER_STANDARD_TRACEBACK": "",
+ },
+ )
+ assert result.returncode != 0
+ assert "URLError" in result.stderr
+ # The user's own code should still appear with full context
+ assert "urllib.request.urlopen" in result.stderr
+ # Suppressed output is ~28 lines (only user's frame + collapsed library frames).
+ # Without suppression it would be ~59 lines (all frames expanded with source context).
+ # Threshold of 35 is generous enough for formatting changes but catches unsuppressed output.
+ assert len(result.stderr.splitlines()) < 35
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/typer/main.py b/typer/main.py
index f4f21bb844..d40746faa1 100644
--- a/typer/main.py
+++ b/typer/main.py
@@ -5,13 +5,13 @@
import subprocess
import sys
import traceback
-from collections.abc import Callable, Sequence
+from collections.abc import Callable, Iterable, Sequence
from datetime import datetime
from enum import Enum
from functools import update_wrapper
from pathlib import Path
from traceback import FrameSummary, StackSummary
-from types import TracebackType
+from types import ModuleType, TracebackType
from typing import Annotated, Any
from uuid import UUID
@@ -514,6 +514,24 @@ def callback():
"""
),
] = True,
+ pretty_exceptions_suppress: Annotated[
+ Iterable[str | ModuleType],
+ Doc(
+ """
+ A list of modules or module path strings to suppress in Rich tracebacks.
+ Frames from these modules will be hidden in the traceback output.
+
+ **Example**
+
+ ```python
+ import typer
+ import httpx
+
+ app = typer.Typer(pretty_exceptions_suppress=(httpx,))
+ ```
+ """
+ ),
+ ] = (),
):
self._add_completion = add_completion
self.rich_markup_mode: MarkupMode = rich_markup_mode
@@ -522,6 +540,7 @@ def callback():
self.pretty_exceptions_enable = pretty_exceptions_enable
self.pretty_exceptions_show_locals = pretty_exceptions_show_locals
self.pretty_exceptions_short = pretty_exceptions_short
+ self.pretty_exceptions_suppress = pretty_exceptions_suppress
self.info = TyperInfo(
name=name,
cls=cls,
@@ -1147,6 +1166,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any:
pretty_exceptions_enable=self.pretty_exceptions_enable,
pretty_exceptions_show_locals=self.pretty_exceptions_show_locals,
pretty_exceptions_short=self.pretty_exceptions_short,
+ pretty_exceptions_suppress=self.pretty_exceptions_suppress,
),
)
raise e
diff --git a/typer/models.py b/typer/models.py
index 3285a96a24..09b52643d0 100644
--- a/typer/models.py
+++ b/typer/models.py
@@ -1,6 +1,7 @@
import inspect
import io
-from collections.abc import Callable, Sequence
+from collections.abc import Callable, Iterable, Sequence
+from types import ModuleType
from typing import (
TYPE_CHECKING,
Any,
@@ -634,10 +635,12 @@ def __init__(
pretty_exceptions_enable: bool = True,
pretty_exceptions_show_locals: bool = True,
pretty_exceptions_short: bool = True,
+ pretty_exceptions_suppress: Iterable[str | ModuleType] = (),
) -> None:
self.pretty_exceptions_enable = pretty_exceptions_enable
self.pretty_exceptions_show_locals = pretty_exceptions_show_locals
self.pretty_exceptions_short = pretty_exceptions_short
+ self.pretty_exceptions_suppress = pretty_exceptions_suppress
class TyperPath(click.Path):
diff --git a/typer/rich_utils.py b/typer/rich_utils.py
index d85043238c..ecb40e59a8 100644
--- a/typer/rich_utils.py
+++ b/typer/rich_utils.py
@@ -742,12 +742,13 @@ def get_traceback(
exception_config: DeveloperExceptionConfig,
internal_dir_names: list[str],
) -> Traceback:
+ suppress = (*internal_dir_names, *exception_config.pretty_exceptions_suppress)
rich_tb = Traceback.from_exception(
type(exc),
exc,
exc.__traceback__,
show_locals=exception_config.pretty_exceptions_show_locals,
- suppress=internal_dir_names,
+ suppress=suppress,
width=MAX_WIDTH,
)
return rich_tb