Skip to content

diegojromerolopez/mypy-raise

Repository files navigation

mypy-raise

A mypy plugin that enforces exception declarations in function signatures, ensuring functions explicitly declare all exceptions they may raise.

test License Maintenance made-with-python Code style: black Imports: isort PyPI pyversions PyPI version mypy-raise PyPI status PyPI download month

Overview

mypy-raise helps you write more reliable Python code by tracking exception propagation through your codebase. Similar to how mypy-pure tracks function purity, mypy-raise ensures that functions declare all exceptions they might raise, including those from called functions and standard library operations.

Features

  • Exception Propagation Analysis - Tracks exceptions through function call chains
  • Standard Library Support - Knows about 100+ stdlib functions and their exceptions
  • Try-Except Analysis - Smartly handles caught exceptions
  • Rich Error Messages - Hints, source locations, and colored output
  • Strict Mode - Enforce exception declarations across the codebase
  • Statistics - Summary of analysis results and compliance rate
  • Configurable - Extend exception mappings and ignore patterns via mypy.ini
  • Zero Runtime Overhead - Pure static analysis, no runtime cost
  • Comprehensive Coverage - High test coverage with verified correctness

Installation

pip install mypy-raise

Quick Start

1. Add the plugin to your mypy.ini or pyproject.toml:

mypy.ini:

[mypy]
plugins = mypy_raise.plugin

pyproject.toml:

[tool.mypy]
plugins = ["mypy_raise.plugin"]

2. Decorate your functions with @raising:

from mypy_raise import raising

@raising(exceptions=[])  # Declares this function raises no exceptions
def safe_calculation(x: int, y: int) -> int:
    return x + y

@raising(exceptions=[ValueError, TypeError])  # Declares possible exceptions
def risky_operation(x: str) -> int:
    if not x.isdigit():
        raise ValueError("Not a number")
    return int(x)

3. Run mypy:

mypy your_code.py

Examples

✅ Correct Usage

from mypy_raise import raising

@raising(exceptions=[])
def add(a: int, b: int) -> int:
    """Pure calculation - no exceptions."""
    return a + b

@raising(exceptions=[ValueError, TypeError])
def parse_number(s: str) -> int:
    """Correctly declares all exceptions."""
    if not isinstance(s, str):
        raise TypeError("Must be a string")
    if not s.isdigit():
        raise ValueError("Not a number")
    return int(s)

@raising(exceptions=[FileNotFoundError, PermissionError, OSError])
def read_config(filename: str) -> str:
    """Declares exceptions from stdlib function open()."""
    with open(filename) as f:
        return f.read()

❌ Detected Violations

from mypy_raise import raising

@raising(exceptions=[])
def unsafe_read(filename: str) -> str:
    # Error: Function 'unsafe_read' may raise 'FileNotFoundError', 'PermissionError', 'OSError'
    # but these are not declared. Raised by: 'builtins.open' raises ...
    with open(filename) as f:
        return f.read()

@raising(exceptions=[ValueError])
def incomplete_declaration(x: str) -> int:
    # Error: Function 'incomplete_declaration' may raise 'TypeError'
    # but these are not declared.
    if not isinstance(x, str):
        raise TypeError("Must be a string")  # Not declared!
    return int(x)

@raising(exceptions=[])
def calls_unsafe(x: str) -> int:
    # Error: Function 'calls_unsafe' may raise 'ValueError', 'TypeError'
    # but these are not declared. Raised by: 'parse_number' raises ...
    return parse_number(x)

Advanced Usage

Configuration

Strict Mode

Enforce that ALL functions must have the @raising decorator. Useful for gradual adoption or ensuring complete coverage.

mypy.ini:

[mypy-raise]
strict = true

Ignore Patterns

Exclude specific files or functions from analysis.

mypy.ini:

[mypy-raise]
# Comma-separated glob patterns
ignore_functions = test_*, _private_*, *deprecated*
ignore_files = tests/*, *_test.py, legacy/*.py

Custom Exceptions

Add custom exception mappings in mypy.ini:

[mypy-raise]
exceptions_my_function = CustomError,AnotherError
exceptions_third_party_lib.function = SomeException

Alternatively, you can use the cleaner multiline syntax with known_exceptions:

[mypy-raise]
known_exceptions =
    my_function: CustomError, AnotherError
    third_party_lib.function: SomeException
    requests.get: requests.RequestException, ValueError

Exception Hierarchy

mypy-raise understands exception inheritance for both try-except blocks and @raising declarations.

Polymorphic Declarations: You can declare a base exception to cover any subclass raised by the function.

@raising(exceptions=[Exception])  # Covers ValueError because it inherits from Exception
def generic_raiser():
    raise ValueError("Something went wrong")

@raising(exceptions=[OSError])    # Covers FileNotFoundError
def file_op():
    raise FileNotFoundError("File missing")

Smart Catching: Catching a base exception correctly handles raised subclasses.

@raising(exceptions=[])  # No exceptions raised out of this function
def safe_handler():
    try:
        raise ValueError("Oops")
    except Exception:     # Correctly identifies that ValueError is handled
        pass

Exception Propagation

The plugin automatically tracks exception propagation through multiple call levels:

@raising(exceptions=[ValueError])
def level3():
    raise ValueError("Error at level 3")

@raising(exceptions=[ValueError])
def level2():
    level3()  # Propagates ValueError

@raising(exceptions=[])
def level1():
    # Error: Indirectly raises ValueError through level2 -> level3
    level2()

Standard Library Support

The plugin includes comprehensive exception mappings for 100+ standard library functions:

@raising(exceptions=[FileNotFoundError, PermissionError, OSError])
def use_open(path: str):
    return open(path).read()

@raising(exceptions=[ValueError, TypeError])
def use_int(s: str):
    return int(s)

@raising(exceptions=[KeyError])
def use_dict_getitem(d: dict, key: str):
    return d[key]

@raising(exceptions=[])  # dict.get never raises
def use_dict_get(d: dict, key: str):
    return d.get(key)

Supported Function Types

  • ✅ Regular functions
  • ✅ Methods
  • ✅ Class methods (@classmethod)
  • ✅ Static methods (@staticmethod)
  • ✅ Async functions
  • ✅ Nested functions

Limitations

mypy-raise performs static analysis and has some limitations:

What it CAN detect:

  • ✅ Direct exception raises
  • ✅ Exceptions from decorated functions
  • ✅ Exceptions from standard library functions
  • ✅ Indirect exception propagation through call chains

What it CANNOT detect:

  • ❌ Exceptions from undecorated functions
  • ❌ Exceptions from third-party libraries (unless configured)
  • ❌ Dynamic raises (e.g., raise getattr(module, exc_name))
  • ❌ Exceptions from eval(), exec(), etc.

Recommendation: Use mypy-raise as a helpful guard rail for critical code paths. Combine it with comprehensive testing.

Development

# Clone the repository
git clone https://github.com/diegojromerolopez/mypy-raise.git
cd mypy-raise

# Install dependencies with uv
pip install uv
uv sync --all-groups

# Run tests
uv run python -m unittest discover -s tests

# Run mypy
uv run mypy mypy_raise/

# Check coverage
uv run coverage run --source=mypy_raise -m unittest discover -s tests
uv run coverage report

Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

License

MIT License - see LICENSE for details.

Acknowledgments

This project was inspired by mypy-pure and created with the assistance of AI tools (Claude Sonnet 4.5 and Antigravity/Gemini 3 Pro).

Related Projects

  • mypy - Optional static typing for Python.
  • mypy-pure - Enforce function purity.
  • mypy-plugins-examples - A project that contains some examples for my mypy-pure and mypy-raise plugins.

Made with ❤️ for the Python community

About

A mypy extension that provides a decorator to mark functions that raise exceptions

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Languages