Skip to main content

mypy-pure

test License Maintenance made-with-python Code style: black Imports: isort PyPI pyversions PyPI version mypy-pure PyPI status PyPI download month

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.

Why mypy-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.

What is a Pure Function?

A pure function:

  1. Always returns the same output for the same inputs (deterministic)
  2. 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 + y

Installation

pip install mypy-pure

Enable the plugin in your mypy.ini or pyproject.toml:

[mypy]
plugins = mypy_pure.plugin

Quick Start

Mark 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.py

Examples

✅ Valid Pure Functions

from 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)

❌ Detected Impurity Violations

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 + y

More examples

More examples can be found in the mypy-pure-examples repository.

Advanced Usage

Configuration Options

mypy-pure supports two configuration options in the [mypy-pure] section of your mypy.ini:

1. impure_functions (Blacklist)

Add custom impure functions that should be flagged as side-effecting:

[mypy-pure]
impure_functions = my_module.send_email, analytics.track_event, cache.set

Use 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 user

2. pure_functions (Whitelist)

Mark third-party library functions as pure, overriding the default assumption:

[mypy-pure]
pure_functions = requests.utils.quote, pandas.DataFrame.copy, my_lib.helper

Use 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)

Combining Both

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.unquote

Priority: pure_functions (whitelist) takes precedence over impure_functions (blacklist).

Library Authors: Auto-Discovery with __mypy_pure__

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.

Declaring Pure Functions

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)

User Experience

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

Benefits

  • 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

Cross-Module References

Reference functions from other modules using fully qualified names:

[mypy-pure]
impure_functions = external_lib.impure_function

Supported Function Types

mypy-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

Built-in Impurity Detection

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...

See full blacklist

Limitations

mypy-pure performs static analysis and has some limitations:

What it CAN detect:

  • ✅ Direct calls to known impure functions
  • ✅ Indirect calls through pure functions calling impure functions
  • ✅ Deeply nested impure calls

What it CANNOT detect:

  • ❌ 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.

Real-World Use Cases

Data Processing Pipelines

@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']),
    }

Business Logic

@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

Configuration Processing

@pure
def merge_configs(default: dict, user: dict) -> dict:
    """Pure config merging - no file I/O."""
    return {**default, **user}

Contributing

Contributions are welcome! Here's how you can help:

  1. Expand the blacklist - Add more impure functions from stdlib or popular libraries
  2. Report bugs - Open an issue if you find incorrect behavior
  3. Suggest features - Ideas for improving purity detection
  4. Improve documentation - Help make the docs clearer

See CONTRIBUTING.md for details.

Development

# 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_pure

License

MIT License - see LICENSE for details.

Acknowledgments

This project was created with the assistance of AI tools (ChatGPT and Antigravity/Gemini) and manually reviewed and refined.

Related Projects

  • 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

Metadata

Release files for mypy-pure 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mypy-pure 0.2.2
File Size Uploaded Provenance Actions
mypy_pure-0.2.2.tar.gz 22.0 kB With provenance Details for mypy_pure-0.2.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for mypy-pure 0.2.2
File Interpreter ABI Platform Actions
mypy_pure-0.2.2-py3-none-any.whl Python 3 none any Details for mypy_pure-0.2.2-py3-none-any.whl

Total release size: 49.6 kB

Release files / mypy_pure-0.2.2.tar.gz

Download URL mypy_pure-0.2.2.tar.gz
Size 22.0 kB
Tags Source
SHA-256 checksum
How to use checksums
41ff36fbdcf6ad5d374c32f78227b5d5bd487fbfff50ac4af790b945b33109f1
BLAKE2b-256 checksum
How to use checksums
6f6c44c2f0522e1d356c0f0e845854145f3ba6054b318cca203ff33475f31a04
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 12, 2025.

Transparency log

Release files / mypy_pure-0.2.2-py3-none-any.whl

Download URL mypy_pure-0.2.2-py3-none-any.whl
Size 27.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
045db827b8dfa2ea5263f0e49d96e085ccb884c1f8dd740372f0426c3781d273
BLAKE2b-256 checksum
How to use checksums
7c2e72da7d69ac10faeec10ff317695f72f19e53c6ca4d7f71623a2184ea5844
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 12, 2025.

Transparency log

Security for mypy-pure 0.2.2

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for mypy_pure-0.2.2.tar.gz

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 12, 2025.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page