A mypy plugin that enforces exception declarations in function signatures, ensuring functions explicitly declare all exceptions they may raise.
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.
- ✅ 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
pip install mypy-raisemypy.ini:
[mypy]
plugins = mypy_raise.pluginpyproject.toml:
[tool.mypy]
plugins = ["mypy_raise.plugin"]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)mypy your_code.pyfrom 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()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)Enforce that ALL functions must have the @raising decorator. Useful for gradual adoption or ensuring complete coverage.
mypy.ini:
[mypy-raise]
strict = trueExclude 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/*.pyAdd custom exception mappings in mypy.ini:
[mypy-raise]
exceptions_my_function = CustomError,AnotherError
exceptions_third_party_lib.function = SomeExceptionAlternatively, 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, ValueErrormypy-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
passThe 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()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)- ✅ Regular functions
- ✅ Methods
- ✅ Class methods (
@classmethod) - ✅ Static methods (
@staticmethod) - ✅ Async functions
- ✅ Nested functions
mypy-raise performs static analysis and has some limitations:
- ✅ Direct exception raises
- ✅ Exceptions from decorated functions
- ✅ Exceptions from standard library functions
- ✅ Indirect exception propagation through call chains
- ❌ 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.
# 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 reportContributions are welcome! See CONTRIBUTING.md for guidelines.
MIT License - see LICENSE for details.
This project was inspired by mypy-pure and created with the assistance of AI tools (Claude Sonnet 4.5 and Antigravity/Gemini 3 Pro).
- 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