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.2.0.tar.gz (24.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.2.0-py3-none-any.whl (31.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: mypy_raise-0.2.0.tar.gz
  • Upload date:
  • Size: 24.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.2.0.tar.gz
Algorithm Hash digest
SHA256 7313048e50cdfe295561131072eb1d49c23149761b90f9245076554b9a387a5d
MD5 3f458557c2db922464890ae5ac39b5a6
BLAKE2b-256 e1e2d041f9dffccc601ed1eb2e69cc4cf20d9133c1fb03d0e22915f93869668d

See more details on using hashes here.

Provenance

The following attestation bundles were made for mypy_raise-0.2.0.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.2.0-py3-none-any.whl.

File metadata

  • Download URL: mypy_raise-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 31.2 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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1a5f64280960f2e9ce1ff6a53dae6119f0409c33471e086d6860df4515fb7269
MD5 56311b45d8b02ad83c548f0759ed1257
BLAKE2b-256 134790988efda5c2a48eaa8f65f39828db1f7de03d4ed7d0e9ab507957be853f

See more details on using hashes here.

Provenance

The following attestation bundles were made for mypy_raise-0.2.0-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