Skip to main content

A runtime type validation utility with detailed error reporting.

Project description

Type Enforcer

PyPI version Python versions License: MIT Test Coverage Code style: ruff

A blazing-fast runtime type validation library for Python 3.13+ that bridges the gap between static type hints and runtime safety. Perfect for validating API responses, user inputs, configurations, and any external data entering your application.

🚀 Why Type Enforcer?

The Challenge

Python's type hints are powerful but only enforced during development. At runtime, your carefully typed code can still receive incorrectly structured data, leading to:

  • Silent Failures: Incorrect data types causing subtle bugs deep in your code
  • Poor Error Messages: Generic AttributeError or TypeError that don't explain what went wrong
  • Security Risks: Unvalidated external input potentially breaking your application logic
  • Debugging Nightmares: Hours spent tracking down where bad data entered your system

Our Solution

Type Enforcer provides bulletproof runtime validation with:

# Without Type Enforcer - Runtime explosion 💥
def process_user(user_data: dict) -> str:
    # This might fail deep in your code with a cryptic error
    return f"Welcome {user_data['name'].upper()}, age {user_data['age']}"

# With Type Enforcer - Safe and validated ✅
from type_enforcer import enforce
from typing import TypedDict

class User(TypedDict):
    name: str
    age: int

def process_user(user_data: dict) -> str:
    # Validates structure and types before processing
    user = enforce(user_data, User)
    return f"Welcome {user.name.upper()}, age {user.age}"

✨ Features

  • 🎯 Runtime Type Validation: Enforce type constraints when it matters most - at runtime
  • 🔍 Crystal Clear Errors: Path-based error messages like data.users[0].email: Expected str, got int
  • 🏗️ Complex Type Support: TypedDict, dataclass, Union, Literal, Enum, and all standard types
  • 🔄 Smart Type Conversion: Automatically converts compatible types (dict → dataclass, str → enum)
  • Optimized Performance: Intelligent caching for repeated validations
  • 🐍 Modern Python: Built for Python 3.13+ with full support for new syntax like X | Y unions
  • Battle-Tested: 100% test coverage with extensive edge case handling

📦 Installation

# Using pip
pip install divine-type-enforcer

# Using uv
uv add divine-type-enforcer

# For development
git clone https://github.com/divine/type-enforcer
cd type-enforcer
uv sync

Requirements

  • Python 3.13+
  • No external dependencies! 🎉

🚀 Quick Start

from type_enforcer import enforce, ValidationError
from typing import TypedDict, Optional

class UserProfile(TypedDict):
    username: str
    email: str
    age: int
    is_active: bool
    metadata: Optional[dict[str, str]]

# Validate API response
try:
    response_data = {
        "username": "john_doe",
        "email": "john@example.com",
        "age": 30,
        "is_active": True,
        "metadata": {"role": "admin"}
    }

    user = enforce(response_data, UserProfile)
    print(f"Validated user: {user['username']}")

except ValidationError as e:
    print(f"Validation failed: {e}")

📖 Usage Examples

Basic Type Validation

from type_enforcer import enforce, ValidationError

# Simple types
enforce(42, int)                    # ✅ Returns: 42
enforce(3.14, float)                # ✅ Returns: 3.14
enforce("hello", str)               # ✅ Returns: "hello"
enforce(True, bool)                 # ✅ Returns: True

# Type mismatches raise ValidationError
try:
    enforce("not a number", int)
except ValidationError as e:
    print(e)  # ": Expected int, got str"

Collections and Nested Structures

from typing import List, Dict, Tuple

# Lists with type checking
users = ["Alice", "Bob", "Charlie"]
enforce(users, List[str])           # ✅ Valid

numbers = [1, 2, "three"]
try:
    enforce(numbers, List[int])
except ValidationError as e:
    print(e)  # "[2]: Expected int, got str"

# Nested dictionaries
config = {
    "database": {
        "host": "localhost",
        "port": 5432,
        "credentials": {
            "user": "admin",
            "password": "secret"
        }
    }
}

ConfigType = Dict[str, Dict[str, int | str | Dict[str, str]]]
enforce(config, ConfigType)         # ✅ Valid

# Fixed-length tuples
point = (10, 20)
enforce(point, Tuple[int, int])     # ✅ Returns: (10, 20)

# Variable-length tuples
numbers = (1, 2, 3, 4, 5)
enforce(numbers, Tuple[int, ...])   # ✅ Valid

Advanced Type Validation

from typing import Union, Optional, Literal
from enum import Enum
from dataclasses import dataclass

# Union types (including new | syntax)
enforce(42, int | str)              # ✅ Returns: 42
enforce("hello", int | str)         # ✅ Returns: "hello"

# Optional types
enforce(None, Optional[str])        # ✅ Returns: None
enforce("value", Optional[str])     # ✅ Returns: "value"

# Literal types
enforce("small", Literal["small", "medium", "large"])  # ✅ Valid

# Enums with automatic conversion
class Status(Enum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"

enforce("PENDING", Status)          # ✅ Returns: Status.PENDING
enforce(Status.APPROVED, Status)    # ✅ Returns: Status.APPROVED

# Dataclasses with dict conversion
@dataclass
class Point:
    x: float
    y: float

enforce({"x": 10, "y": 20}, Point)  # ✅ Returns: Point(x=10.0, y=20.0)

Real-World Example: API Response Validation

from typing import TypedDict, Optional, List
from datetime import datetime
from type_enforcer import enforce, ValidationError

class Address(TypedDict):
    street: str
    city: str
    country: str
    postal_code: Optional[str]

class UserResponse(TypedDict):
    id: int
    username: str
    email: str
    created_at: str
    is_verified: bool
    profile: dict[str, str | int]
    addresses: List[Address]
    tags: Optional[List[str]]

# Simulated API response
api_response = {
    "id": 12345,
    "username": "jane_doe",
    "email": "jane@example.com",
    "created_at": "2024-01-15T10:30:00Z",
    "is_verified": True,
    "profile": {
        "age": 28,
        "bio": "Software engineer",
        "location": "San Francisco"
    },
    "addresses": [
        {
            "street": "123 Main St",
            "city": "San Francisco",
            "country": "USA",
            "postal_code": "94105"
        }
    ],
    "tags": ["developer", "python", "ai"]
}

try:
    # Validate the entire response structure
    validated_user = enforce(api_response, UserResponse)

    # Now you can safely access all fields
    print(f"User {validated_user['username']} joined on {validated_user['created_at']}")
    print(f"Primary city: {validated_user['addresses'][0]['city']}")

except ValidationError as e:
    # Detailed error showing exact path to the problem
    print(f"API response validation failed: {e}")
    # Example error: "addresses[0].postal_code: Expected str | None, got int"

Configuration File Validation

from typing import TypedDict, Literal, Optional

class DatabaseConfig(TypedDict):
    host: str
    port: int
    username: str
    password: str
    database: str
    pool_size: Optional[int]

class LoggingConfig(TypedDict):
    level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]
    format: str
    file: Optional[str]

class AppConfig(TypedDict):
    environment: Literal["development", "staging", "production"]
    debug: bool
    database: DatabaseConfig
    logging: LoggingConfig
    features: dict[str, bool]

# Load configuration (from JSON, YAML, etc.)
config_data = {
    "environment": "production",
    "debug": False,
    "database": {
        "host": "db.example.com",
        "port": 5432,
        "username": "app_user",
        "password": "secure_password",
        "database": "myapp",
        "pool_size": 20
    },
    "logging": {
        "level": "INFO",
        "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        "file": "/var/log/myapp.log"
    },
    "features": {
        "new_ui": True,
        "beta_api": False,
        "analytics": True
    }
}

# Validate configuration on startup
try:
    config = enforce(config_data, AppConfig)
    print(f"✅ Configuration valid for {config['environment']} environment")
except ValidationError as e:
    print(f"❌ Configuration error: {e}")
    exit(1)

🛡️ Error Handling

Type Enforcer provides detailed, actionable error messages:

from type_enforcer import enforce, ValidationError
from typing import TypedDict, List

class Product(TypedDict):
    id: int
    name: str
    price: float
    tags: List[str]

# Example with multiple errors
invalid_data = {
    "id": "not-a-number",  # Wrong type
    "name": "Laptop",
    "price": "2999.99",    # String instead of float
    "tags": ["electronics", 123, "computers"]  # Mixed types
}

try:
    enforce(invalid_data, Product)
except ValidationError as e:
    print(f"Validation error: {e}")
    # Output: "id: Expected int, got str"

    # The error message pinpoints the exact location of the first error

🎯 Best Practices

1. Validate at System Boundaries

from type_enforcer import enforce
from typing import TypedDict

class RequestPayload(TypedDict):
    action: str
    data: dict[str, any]

@app.route("/api/process", methods=["POST"])
def process_request():
    try:
        # Validate incoming data immediately
        payload = enforce(request.json, RequestPayload)
        return handle_action(payload["action"], payload["data"])
    except ValidationError as e:
        return {"error": f"Invalid request: {e}"}, 400

2. Create Reusable Type Definitions

# types.py
from typing import TypedDict, Optional, Literal

class Money(TypedDict):
    amount: float
    currency: Literal["USD", "EUR", "GBP"]

class BaseEntity(TypedDict):
    id: int
    created_at: str
    updated_at: str

class User(BaseEntity):
    email: str
    name: str
    balance: Money
    preferences: Optional[dict[str, any]]

3. Combine with Default Values

from type_enforcer import enforce
from typing import TypedDict, Optional

class Settings(TypedDict, total=False):
    theme: Literal["light", "dark"]
    notifications: bool
    language: str

DEFAULT_SETTINGS: Settings = {
    "theme": "light",
    "notifications": True,
    "language": "en"
}

def load_user_settings(user_data: dict) -> Settings:
    # Merge with defaults and validate
    settings = {**DEFAULT_SETTINGS, **user_data}
    return enforce(settings, Settings)

🔧 Development

Setup

# Clone the repository
git clone https://github.com/divinescreener/type-enforcer
cd type-enforcer

# Install dependencies
uv sync

# Run tests
uv run pytest

# Run linting
uv run ruff check
uv run mypy src

Contributing

We welcome contributions! Please see our Contributing Guidelines for details.

Key points:

  • Maintain 100% test coverage
  • Follow the existing code style
  • Add tests for new features
  • Update documentation as needed

📊 Performance

Type Enforcer is designed for performance:

  • Caching: Type introspection results are cached for repeated validations
  • Early Exit: Validation stops at the first error for efficiency
  • Minimal Dependencies: Zero external dependencies means fast imports
  • Optimized Paths: Common types (int, str, etc.) use fast paths

🔒 Security

  • Safe for validating untrusted input
  • No code execution during validation
  • No arbitrary object instantiation
  • Memory-efficient for large data structures

📄 License

MIT License - see LICENSE for details.

🙏 Acknowledgments

  • Built for the modern Python ecosystem
  • Inspired by the need for runtime type safety in production applications
  • Special thanks to the Python typing community

Made with ❤️ by DIVINE

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

divine_type_enforcer-0.1.13.tar.gz (43.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

divine_type_enforcer-0.1.13-py3-none-any.whl (11.4 kB view details)

Uploaded Python 3

File details

Details for the file divine_type_enforcer-0.1.13.tar.gz.

File metadata

  • Download URL: divine_type_enforcer-0.1.13.tar.gz
  • Upload date:
  • Size: 43.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for divine_type_enforcer-0.1.13.tar.gz
Algorithm Hash digest
SHA256 59a26f47b919a76c3930b94eec28a13441cc09303241b4c58b4f406b2e877605
MD5 793b29a898cc6dbb9948f9a03551d895
BLAKE2b-256 8876151829d209b746aae0975b86f0c08070ddceb15d6896437b309dd52b9ee0

See more details on using hashes here.

Provenance

The following attestation bundles were made for divine_type_enforcer-0.1.13.tar.gz:

Publisher: pypi-publish.yml on divinescreener/type-enforcer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file divine_type_enforcer-0.1.13-py3-none-any.whl.

File metadata

File hashes

Hashes for divine_type_enforcer-0.1.13-py3-none-any.whl
Algorithm Hash digest
SHA256 b77d304232dba6c4b32a343b00e61e3ff0eb359045ade447c7e3f97a90ea2cb4
MD5 1686bffc13f4b86aa7258ef515c7febe
BLAKE2b-256 2d2bbac4fe3544697162231d8c32c11043f2a140d88bfb4d4de81865d18cf907

See more details on using hashes here.

Provenance

The following attestation bundles were made for divine_type_enforcer-0.1.13-py3-none-any.whl:

Publisher: pypi-publish.yml on divinescreener/type-enforcer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page