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.
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7313048e50cdfe295561131072eb1d49c23149761b90f9245076554b9a387a5d
|
|
| MD5 |
3f458557c2db922464890ae5ac39b5a6
|
|
| BLAKE2b-256 |
e1e2d041f9dffccc601ed1eb2e69cc4cf20d9133c1fb03d0e22915f93869668d
|
Provenance
The following attestation bundles were made for mypy_raise-0.2.0.tar.gz:
Publisher:
publish_on_pypi.yml on diegojromerolopez/mypy-raise
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mypy_raise-0.2.0.tar.gz -
Subject digest:
7313048e50cdfe295561131072eb1d49c23149761b90f9245076554b9a387a5d - Sigstore transparency entry: 763977430
- Sigstore integration time:
-
Permalink:
diegojromerolopez/mypy-raise@deb5ac0891a080000eeebe2b4ec92c09ca05e5d3 -
Branch / Tag:
refs/tags/0.2.0 - Owner: https://github.com/diegojromerolopez
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish_on_pypi.yml@deb5ac0891a080000eeebe2b4ec92c09ca05e5d3 -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a5f64280960f2e9ce1ff6a53dae6119f0409c33471e086d6860df4515fb7269
|
|
| MD5 |
56311b45d8b02ad83c548f0759ed1257
|
|
| BLAKE2b-256 |
134790988efda5c2a48eaa8f65f39828db1f7de03d4ed7d0e9ab507957be853f
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mypy_raise-0.2.0-py3-none-any.whl -
Subject digest:
1a5f64280960f2e9ce1ff6a53dae6119f0409c33471e086d6860df4515fb7269 - Sigstore transparency entry: 763977431
- Sigstore integration time:
-
Permalink:
diegojromerolopez/mypy-raise@deb5ac0891a080000eeebe2b4ec92c09ca05e5d3 -
Branch / Tag:
refs/tags/0.2.0 - Owner: https://github.com/diegojromerolopez
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish_on_pypi.yml@deb5ac0891a080000eeebe2b4ec92c09ca05e5d3 -
Trigger Event:
release
-
Statement type: