mypy-raise
A mypy plugin that enforces exception declarations in function signatures, ensuring functions explicitly declare all exceptions they may raise.
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
Metadata
Release files for mypy-raise 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mypy_raise-0.2.0.tar.gz | 24.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mypy_raise-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 55.4 kB
Release files / mypy_raise-0.2.0.tar.gz
| Download URL | mypy_raise-0.2.0.tar.gz |
|---|---|
| Size | 24.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7313048e50cdfe295561131072eb1d49c23149761b90f9245076554b9a387a5d
|
|
BLAKE2b-256 checksum How to use checksums |
e1e2d041f9dffccc601ed1eb2e69cc4cf20d9133c1fb03d0e22915f93869668d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Dec 14, 2025.
Transparency logRelease files / mypy_raise-0.2.0-py3-none-any.whl
| Download URL | mypy_raise-0.2.0-py3-none-any.whl |
|---|---|
| Size | 31.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1a5f64280960f2e9ce1ff6a53dae6119f0409c33471e086d6860df4515fb7269
|
|
BLAKE2b-256 checksum How to use checksums |
134790988efda5c2a48eaa8f65f39828db1f7de03d4ed7d0e9ab507957be853f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Dec 14, 2025.
Transparency log