Enforce functional purity in Python with static type checking.
A mypy plugin that helps you write safer, more predictable code by detecting side effects in functions marked as @pure.
Pure functions are:
- Easier to test - No mocks needed, same inputs always give same outputs
- Easier to reason about - No hidden state changes or side effects
- Easier to refactor - Can be moved, renamed, or reordered safely
- Easier to parallelize - No race conditions or shared state issues
- Easier to cache - Results can be memoized safely
But enforcing purity manually is error-prone. mypy-pure catches impure code at type-check time, before it reaches production.
A pure function:
- Always returns the same output for the same inputs (deterministic)
- Has no side effects (no I/O, no mutations, no external state changes)
# ✅ Pure - deterministic, no side effects
def add(x: int, y: int) -> int:
return x + y
# ❌ Impure - side effect (I/O)
def add_and_log(x: int, y: int) -> int:
print(f"Adding {x} + {y}") # Side effect!
return x + ypip install mypy-pureEnable the plugin in your mypy.ini or pyproject.toml:
[mypy]
plugins = mypy_pure.pluginMark functions as pure with the @pure decorator:
from mypy_pure import pure
@pure
def calculate_total(prices: list[float], tax_rate: float) -> float:
subtotal = sum(prices)
return subtotal * (1 + tax_rate)Run mypy to check for purity violations:
mypy your_code.pyfrom mypy_pure import pure
@pure
def fibonacci(n: int) -> int:
"""Pure recursive function."""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@pure
def process_data(items: list[dict]) -> list[str]:
"""Pure data transformation."""
return [item['name'].upper() for item in items if item['active']]
@pure
def calculate_discount(price: float, discount_pct: float) -> float:
"""Pure business logic."""
return price * (1 - discount_pct / 100)from mypy_pure import pure
import os
@pure
def read_config() -> dict:
# Error: Function 'read_config' is annotated as pure but calls impure functions.
with open('config.json') as f: # I/O is impure!
return json.load(f)
@pure
def delete_temp_files(directory: str) -> None:
# Error: Function 'delete_temp_files' is annotated as pure but calls impure functions.
for file in os.listdir(directory):
os.remove(file) # File system modification is impure!
@pure
def log_and_calculate(x: int, y: int) -> int:
# Error: Function 'log_and_calculate' is annotated as pure but calls impure functions.
print(f"Calculating {x} + {y}") # Logging is impure!
return x + yMore examples can be found in the mypy-pure-examples repository.
mypy-pure supports two configuration options in the [mypy-pure] section of your mypy.ini:
Add custom impure functions that should be flagged as side-effecting:
[mypy-pure]
impure_functions = my_module.send_email, analytics.track_event, cache.setUse cases:
- Your own functions that have side effects
- Third-party library functions not in the built-in blacklist
- Project-specific impure operations
Example:
# my_module.py
def send_email(to: str, subject: str) -> None:
# Sends an email (side effect)
...
# main.py
from mypy_pure import pure
from my_module import send_email
@pure
def process_user(user: dict) -> dict:
send_email(user['email'], 'Welcome') # ❌ Error: calls impure function
return userMark third-party library functions as pure, overriding the default assumption:
[mypy-pure]
pure_functions = requests.utils.quote, pandas.DataFrame.copy, my_lib.helperUse cases:
- Pure utility functions from third-party libraries
- Functions you've verified have no side effects
- Overriding false positives
Example:
from mypy_pure import pure
import requests.utils
@pure
def sanitize_url(url: str) -> str:
# OK because requests.utils.quote is in pure_functions config
return requests.utils.quote(url)You can use both options together:
[mypy-pure]
# Blacklist your impure functions
impure_functions = my_module.send_email, my_module.log_event
# Whitelist pure third-party functions
pure_functions = requests.utils.quote, requests.utils.unquotePriority: pure_functions (whitelist) takes precedence over impure_functions (blacklist).
If you're a library author, you can declare your pure functions using the __mypy_pure__ module-level list. This enables zero-configuration purity checking for your users.
Add a __mypy_pure__ list to your module:
# my_library.py
__mypy_pure__ = [
'pure_helper',
'utils.calculate',
'ClassName.method_name',
]
def pure_helper(x: int) -> int:
"""A pure utility function."""
return x * 2
class utils:
@staticmethod
def calculate(a: int, b: int) -> int:
"""A pure calculation."""
return a + b
def impure_logger(msg: str) -> None:
"""Not in __mypy_pure__, so treated as impure."""
print(msg)Users of your library automatically benefit without any configuration:
from mypy_pure import pure
import my_library
@pure
def process(x: int) -> int:
# ✅ OK - pure_helper is in __mypy_pure__
return my_library.pure_helper(x)
@pure
def log_process(x: int) -> int:
# ❌ Error: Function 'log_process' is impure because it calls 'my_library.impure_logger'
my_library.impure_logger(f"Processing {x}")
return x- Zero configuration for library users
- Self-documenting API - pure functions are explicitly declared
- Compile-time guarantees - purity violations caught during type checking
- Better IDE support - users see which functions are safe to use in pure contexts
Reference functions from other modules using fully qualified names:
[mypy-pure]
impure_functions = external_lib.impure_functionmypy-pure works with all Python function and method types:
- ✅ Regular functions
- ✅ Instance methods
- ✅ Class methods (
@classmethod) - ✅ Static methods (
@staticmethod) - ✅ Async functions (
async def) - ✅ Async methods
- ✅ Property methods (
@property) - ✅ Nested/inner functions
mypy-pure includes a comprehensive blacklist of 200+ impure functions from Python's standard library:
- File I/O:
open(),pathlib.Path.write_text(), etc. - System operations:
os.remove(),subprocess.run(), etc. - Network:
socket.socket(),urllib.request.urlopen(), etc. - Logging:
logging.info(),print(), etc. - State modification:
random.seed(),sys.exit(), etc. - Databases:
sqlite3.connect(), etc. - And many more...
mypy-pure performs static analysis and has some limitations:
- ✅ Direct calls to known impure functions
- ✅ Indirect calls through pure functions calling impure functions
- ✅ Deeply nested impure calls
- ❌ Mutations of mutable arguments (e.g.,
list.append()) - ❌ Global variable modifications
- ❌ Impure functions not in the blacklist
- ❌ Dynamic function calls (e.g.,
getattr(),eval()) - ❌ Side effects in third-party libraries (unless configured)
Recommendation: Use mypy-pure as a helpful guard rail, not a guarantee of purity. Combine it with code reviews and testing.
@pure
def transform_user_data(raw_data: dict) -> dict:
"""Pure transformation - easy to test and parallelize."""
return {
'id': raw_data['user_id'],
'name': raw_data['full_name'].title(),
'age': calculate_age(raw_data['birth_date']),
}@pure
def calculate_shipping_cost(
weight_kg: float,
distance_km: float,
is_express: bool
) -> float:
"""Pure business logic - deterministic and testable."""
base_cost = weight_kg * 0.5 + distance_km * 0.1
return base_cost * 1.5 if is_express else base_cost@pure
def merge_configs(default: dict, user: dict) -> dict:
"""Pure config merging - no file I/O."""
return {**default, **user}Contributions are welcome! Here's how you can help:
- Expand the blacklist - Add more impure functions from stdlib or popular libraries
- Report bugs - Open an issue if you find incorrect behavior
- Suggest features - Ideas for improving purity detection
- Improve documentation - Help make the docs clearer
See CONTRIBUTING.md for details.
# Clone the repository
git clone https://github.com/diegojromerolopez/mypy-pure.git
cd mypy-pure
# Install dependencies
uv sync
# Run tests
pytest
# Run mypy
mypy mypy_pureMIT License - see LICENSE for details.
This project was created with the assistance of AI tools (ChatGPT and Antigravity/Gemini) and manually reviewed and refined.
- mypy - Optional static typing for Python
- mypy-raise - A mypy plugin that enforces exception declarations in function signatures, ensuring functions explicitly declare all exceptions they may raise.
- mypy-plugins-examples - A project that contains some examples for my mypy-pure and mypy-raise plugins.
Made with ❤️ for the Python community