Skip to content

API Reference

Top-level API

Safe exception handling through compile-time and runtime validation of raised exceptions.

raises(*exceptions: type[BaseException]) -> Callable[[T], T]

Declare the exceptions a function may raise.

At runtime, validates that each declared exception is in the current watched set (see enable). Adds the declared exceptions to the watched set for the duration of the call.

Parameters:

Name Type Description Default
*exceptions type[BaseException]

Exception types the decorated function is allowed to raise.

()

Returns:

Type Description
Callable[[T], T]

A decorator that wraps the function with exception validation.

Raises:

Type Description
UnwatchedRaiseError

If a declared exception is not in the watched set.

Source code in src/saferaise/_decorator.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def raises[T: Callable[..., object]](*exceptions: type[BaseException]) -> Callable[[T], T]:
    """Declare the exceptions a function may raise.

    At runtime, validates that each declared exception is in the current
    watched set (see ``enable``). Adds the declared exceptions to the
    watched set for the duration of the call.

    Args:
        *exceptions: Exception types the decorated function is allowed to raise.

    Returns:
        A decorator that wraps the function with exception validation.

    Raises:
        UnwatchedRaiseError: If a declared exception is not in the watched set.
    """

    def decorator(func: T) -> T:
        if inspect.iscoroutinefunction(func):

            @wraps(func)
            async def async_wrapper(*args: object, **kwargs: object) -> object:
                if error := _validate_exceptions(func.__name__, exceptions):
                    raise error.with_traceback(None)
                with watch_exceptions(*exceptions):
                    return await func(*args, **kwargs)

            return async_wrapper  # pyright: ignore[reportReturnType]

        @wraps(func)
        def wrapper(*args: object, **kwargs: object) -> object:
            if error := _validate_exceptions(func.__name__, exceptions):
                raise error.with_traceback(None)
            with watch_exceptions(*exceptions):
                return func(*args, **kwargs)

        return wrapper  # pyright: ignore[reportReturnType]

    return decorator

register(*roots: str) -> None

Install the import hook for the given package roots.

Must be called before importing the packages you want to instrument, and in a separate file from those packages. The hook rewrites every try/except block at import time so that caught exception types are automatically added to the watched set.

Parameters:

Name Type Description Default
*roots str

Top-level package names to instrument (e.g. "myapp").

()
Example
import saferaise

saferaise.register("myapp")  # must come before `import myapp`

import myapp

with saferaise.enable():
    myapp.run()

Raises:

Type Description
NameCollisionError

If an instrumented module already has an attribute named _saferaise_watch_exceptions.

Source code in src/saferaise/_patcher/_hook.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def register(*roots: str) -> None:
    """Install the import hook for the given package roots.

    Must be called **before** importing the packages you want to instrument,
    and in a separate file from those packages. The hook rewrites every
    ``try/except`` block at import time so that caught exception types are
    automatically added to the watched set.

    Args:
        *roots: Top-level package names to instrument (e.g. ``"myapp"``).

    Example:
        ```python
        import saferaise

        saferaise.register("myapp")  # must come before `import myapp`

        import myapp

        with saferaise.enable():
            myapp.run()
        ```

    Raises:
        NameCollisionError: If an instrumented module already has an attribute
            named ``_saferaise_watch_exceptions``.
    """
    sys.meta_path.insert(0, _TryCtxFinder(*roots))

is_registered() -> bool

Check if saferaise is registered for the current file.

Returns:

Name Type Description
bool bool

True if saferaise is registered, False otherwise.

Source code in src/saferaise/_patcher/_common.py
 6
 7
 8
 9
10
11
12
13
14
def is_registered() -> bool:
    """Check if saferaise is registered for the current file.

    Returns:
        bool: True if saferaise is registered, False otherwise.
    """
    if (frame := inspect.currentframe()) and (prev_frame := frame.f_back):
        return WATCHER_KEY in prev_frame.f_globals
    return False

enable() -> Iterator[None]

Enable exception watching with an empty watched set.

Inside this context, any @raises-decorated function will validate that its declared exceptions are being watched. Use this at the top level of your application or in tests to activate enforcement.

Example
import saferaise

saferaise.register("myapp")
import myapp

with saferaise.enable():
    myapp.run()
Source code in src/saferaise/_watched_exceptions.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@contextmanager
def enable() -> Iterator[None]:
    """Enable exception watching with an empty watched set.

    Inside this context, any ``@raises``-decorated function will validate that
    its declared exceptions are being watched. Use this at the top level of
    your application or in tests to activate enforcement.

    Example:
        ```python
        import saferaise

        saferaise.register("myapp")
        import myapp

        with saferaise.enable():
            myapp.run()
        ```
    """
    token = _watched_exceptions.set(frozenset())
    try:
        yield
    finally:
        _watched_exceptions.reset(token)

is_enabled() -> bool

Check if exception watching is currently enabled.

Returns:

Type Description
bool

True if watching is enabled, False otherwise.

Source code in src/saferaise/_watched_exceptions.py
128
129
130
131
132
133
134
def is_enabled() -> bool:
    """Check if exception watching is currently enabled.

    Returns:
        True if watching is enabled, False otherwise.
    """
    return _watched_exceptions.get() is not None

disable() -> Iterator[None]

Temporarily disable exception watching.

Inside this context, @raises validation is skipped entirely, regardless of any outer enable or watch_exceptions context. Use this in production hot paths or during migration when you want instrumentation in place but enforcement off.

Example
import os
from saferaise import enable, disable

ctx = enable if os.getenv("ENV") != "production" else disable
with ctx():
    myapp.run()
Source code in src/saferaise/_watched_exceptions.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@contextmanager
def disable() -> Iterator[None]:
    """Temporarily disable exception watching.

    Inside this context, ``@raises`` validation is skipped entirely,
    regardless of any outer ``enable`` or ``watch_exceptions`` context.
    Use this in production hot paths or during migration when you want
    instrumentation in place but enforcement off.

    Example:
        ```python
        import os
        from saferaise import enable, disable

        ctx = enable if os.getenv("ENV") != "production" else disable
        with ctx():
            myapp.run()
        ```
    """
    token = _watched_exceptions.set(None)
    try:
        yield
    finally:
        _watched_exceptions.reset(token)

unsafe(*exceptions: type[BaseException]) -> Iterator[None]

Add exceptions to the watched set without validating callers.

Useful at bootstrapping boundaries where a try/except would be artificial - for example, entry points or test harnesses that need to manually declare which exceptions are acceptable.

Parameters:

Name Type Description Default
*exceptions type[BaseException]

Exception types to add to the watched set.

()
Example
from saferaise import enable, unsafe, raises


@raises(ValueError)
def parse(raw: str) -> int:
    return int(raw)


with enable():
    with unsafe(ValueError):
        parse("abc")  # OK - ValueError manually added to watched set
Source code in src/saferaise/_watched_exceptions.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
@contextmanager
def unsafe(*exceptions: type[BaseException]) -> Iterator[None]:
    """Add exceptions to the watched set without validating callers.

    Useful at bootstrapping boundaries where a ``try/except`` would be
    artificial - for example, entry points or test harnesses that need
    to manually declare which exceptions are acceptable.

    Args:
        *exceptions: Exception types to add to the watched set.

    Example:
        ```python
        from saferaise import enable, unsafe, raises


        @raises(ValueError)
        def parse(raw: str) -> int:
            return int(raw)


        with enable():
            with unsafe(ValueError):
                parse("abc")  # OK - ValueError manually added to watched set
        ```
    """
    token = _add_exceptions(*exceptions)
    try:
        yield
    finally:
        _reset_exceptions(token)

Errors

All errors inherit from SafeRaiseError, which itself inherits from BaseException rather than Exception. This is intentional: a bare except Exception block should never silently swallow a saferaise violation.

Note

Import error classes directly from saferaise._errors or catch them by type - they are raised as BaseException subclasses, not Exception.

SafeRaiseError

Bases: BaseException

Base error for all saferaise library errors.

Source code in src/saferaise/_errors.py
1
2
class SafeRaiseError(BaseException):
    """Base error for all saferaise library errors."""

UnwatchedRaiseError

Bases: SafeRaiseError

A @raises-decorated function declared exceptions not in the current watched set.

Source code in src/saferaise/_errors.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class UnwatchedRaiseError(SafeRaiseError):
    """A @raises-decorated function declared exceptions not in the current watched set."""

    def __init__(self, func_name: str, declared: tuple[type[BaseException], ...], missing: type[BaseException]) -> None:
        self.func_name: str = func_name
        self.declared: tuple[type[BaseException], ...] = declared
        self.missing: type[BaseException] = missing
        declared_names = ", ".join(e.__name__ for e in declared)
        super().__init__(
            f"Function {func_name} declares raises({declared_names})"
            + f" but {missing.__name__} is not in the current watched set."
            + f" Wrap the call with `unsafe({missing.__name__})` or an appropriate try/except block."
        )

NotEnteredError

Bases: SafeRaiseError

A context manager was exited without being entered.

Source code in src/saferaise/_errors.py
20
21
22
23
24
25
class NotEnteredError(SafeRaiseError):
    """A context manager was exited without being entered."""

    def __init__(self, context_name: str) -> None:
        self.context_name: str = context_name
        super().__init__(f"{context_name} was not entered")

NameCollisionError

Bases: SafeRaiseError

The injected watcher key already exists in a module's namespace.

Source code in src/saferaise/_errors.py
28
29
30
31
32
33
34
class NameCollisionError(SafeRaiseError):
    """The injected watcher key already exists in a module's namespace."""

    def __init__(self, module_name: str, key: str) -> None:
        self.module_name: str = module_name
        self.key: str = key
        super().__init__(f"Module {module_name} already has {key}, possible name collision")