Skip to main content

A mypy plugin to check for raised exceptions

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mypy_raise-0.1.1.tar.gz (21.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mypy_raise-0.1.1-py3-none-any.whl (16.9 kB view details)

Uploaded Python 3

File details

Details for the file mypy_raise-0.1.1.tar.gz.

File metadata

  • Download URL: mypy_raise-0.1.1.tar.gz
  • Upload date:
  • Size: 21.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for mypy_raise-0.1.1.tar.gz
Algorithm Hash digest
SHA256 26b555e1ca30b02732157660c2d04538c0636fae31a8cc1125fb0473d13633fb
MD5 cffd75ebe4c0bea681c7ee675d21f4ad
BLAKE2b-256 a21bd4df7b9eb8675a3b125c26957a6b5177167a6b58849e30f97c64546034ac

See more details on using hashes here.

Provenance

The following attestation bundles were made for mypy_raise-0.1.1.tar.gz:

Publisher: publish_on_pypi.yml on diegojromerolopez/mypy-raise

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mypy_raise-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: mypy_raise-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 16.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for mypy_raise-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 055254ba7db34d6b2282161414b3878344ba25aec14dddd22a3f575e2d328f53
MD5 9cf0ed4023359861718d3d035392c0e2
BLAKE2b-256 f35aba3bad64f5ba282ccb610580769391dd3d7d88f81731a03f9901084b3f00

See more details on using hashes here.

Provenance

The following attestation bundles were made for mypy_raise-0.1.1-py3-none-any.whl:

Publisher: publish_on_pypi.yml on diegojromerolopez/mypy-raise

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page