Skip to main content

type_enforced

PyPI version License: MIT DOI PyPI Downloads

Fast, pure-Python runtime type enforcement for Python 3.11+ type annotations. Zero dependencies and uncompromising performance.


Quick Start

import type_enforced

@type_enforced.Enforcer
def greet(name: str, repeat: int = 1) -> str:
    return f"Hello {name}!" * repeat

greet("Alice", 2)       # Returns "Hello Alice!Hello Alice!"
greet("Alice", "twice")  # Raises TypeError at runtime!

Why type_enforced?

Static type checkers (like mypy or pyright) catch errors during development, but offer zero protection at runtime against dynamic payloads, untyped API inputs, or user data.

Existing runtime type checkers force an unnecessary compromise:

  • Pydantic provides thorough validation, but comes with heavy runtime overhead and steep execution slowdowns.
  • Beartype achieves high speed primarily by taking shortcuts. It samples 1 element in collections and misses invalid items in unsampled data.

type_enforced eliminates this compromise:

  • Guaranteed Complete Validation: Validates every single item across large collections and nested data structures (e.g. list[dict[str, int]] or dicts with 10,000+ keys) by default, with zero shortcuts.
  • Fastest Full Validation: Delivers full, uncompromising validation at a fraction of Pydantic's overhead.
  • Fastest Sampled Validation: Need O(1) or logarithmic sampling for massive collections? This is how Beartype works. Set iterable_sample_pct='first', 'last', 'log', 0 (random pick), or a percentage. Sampled validation in type_enforced runs up to 2x faster than Beartype.
  • Pure Python, Zero Dependencies: A lightweight decorator with zero external packages, C-extensions, or compilation steps. Compatible everywhere Python 3.11+ runs.
  • Rich Type Support & Constraints: Seamlessly supports standard Python | unions, nested generics, Literals, Callables, Dataclasses, custom class inheritance, and custom validation Constraint rules.
  • Clean Tracebacks: Strips internal validation frames from tracebacks by default, pinpointing the exact line in your code that caused the issue.

Performance at a Glance

Timings are averages of a single validation over 100 runs. ⚠ = checker did not consistently catch invalid types for this case (see full benchmarks).

Type Size type_enforced (sample=1) Beartype (sample=1) type_enforced (100%) Pydantic (100%)
int — 0.16 µs 0.28 µs 0.19 µs 1.56 µs
Union[int, float] — 0.17 µs 0.30 µs 0.17 µs 1.52 µs
str — 0.15 µs 0.27 µs 0.16 µs 1.27 µs
list[int] 1 000 items 0.18 µs ⚠ 0.42 µs ⚠ 12.36 µs 22.40 µs
dict[str, int] 1 000 keys 0.27 µs ⚠ 0.43 µs ⚠ 27.48 µs 81.13 µs
dict[str, int] 10 000 keys 0.27 µs ⚠ 0.44 µs ⚠ 269.26 µs 872.03 µs
list[dict[str, int]] 100 x 10 items 0.29 µs ⚠ 0.56 µs ⚠ 43.61 µs 82.00 µs
list[dict[str, int]] 100 x 100 items 0.30 µs ⚠ 0.63 µs ⚠ 313.17 µs 776.74 µs

Sampled Validation: When 1 sample validation is acceptable, type_enforced is up to 2x faster than Beartype.

Full Validation: When full validation is required, type_enforced is up to 8x faster than Pydantic.


Installation

Install via pip:

pip install type_enforced

Or using uv:

uv add type_enforced

Requirements

  • Python 3.11+
  • Zero external runtime dependencies
Legacy Python Compatibility

For older Python versions, pin to legacy releases:

  • Python 3.10: pip install "type_enforced<=1.10.2"
  • Python 3.9: pip install "type_enforced<=1.9.0"
  • Python 3.7 – 3.8: pip install "type_enforced==0.0.16"

Usage Guide

1. Functions and Methods

Apply @type_enforced.Enforcer to any callable. It validates positional arguments, keyword arguments, default parameters, and the return type.

import type_enforced

@type_enforced.Enforcer
def process_user(user_id: int, tags: list[str], active: bool = True) -> dict[str, str | int]:
    return {"user_id": user_id, "status": "active" if active else "inactive"}

# Passing invalid types raises a descriptive TypeError:
process_user("123", ["admin"])
# TypeError: TypeEnforced Exception (process_user): Type mismatch for typed variable `user_id`.
# Expected one of the following `[<class 'int'>]` but got `<class 'str'>` with value `123` instead.

2. Classes and Dataclasses

Decorating a class automatically enforces types on all annotated methods (including __init__, @classmethod, and @staticmethod):

import type_enforced
from dataclasses import dataclass

@type_enforced.Enforcer
class Account:
    def __init__(self, username: str, balance: float):
        self.username = username
        self.balance = balance

    def deposit(self, amount: float) -> float:
        self.balance += amount
        return self.balance

    @staticmethod
    def validate_code(code: str) -> bool:
        return len(code) == 6

# Dataclasses work seamlessly:
@type_enforced.Enforcer
@dataclass
class UserConfig:
    retries: int
    endpoint: str

To disable enforcement on a specific method within an enforced class:

@type_enforced.Enforcer
class Worker:
    def standard_job(self, task: str) -> None:
        pass

    @type_enforced.Enforcer(enabled=False)
    def high_throughput_job(self, data):
        # Type enforcement skipped for maximum throughput
        pass

3. Module-Level Enforcement (ModuleEnforcer)

Enforce typing across an entire module in a single line without decorating every function and class individually:

# Place at the top of your module file (e.g., my_package/core.py)
import type_enforced

type_enforced.ModuleEnforcer()

def add(a: int, b: int) -> int:
    return a + b

class Helper:
    def run(self, flag: bool) -> str:
        return "ok" if flag else "failed"

You can also enforce an imported module:

import my_package
import type_enforced

type_enforced.ModuleEnforcer(my_package)

Note: By default, submodules=True, which recursively enforces all sub-packages/sub-modules in the same namespace (e.g. mypkg.submodule), while safely ignoring third-party and standard library imports.


Supported Type Annotations

type_enforced supports all standard Python 3.11+ typing constructs:

Standard Built-ins & Unions

@type_enforced.Enforcer
def fn(
    a: int,
    b: str | float,                    # Standard union syntax
    c: int | None = None,              # Optional syntax
) -> None:
    pass

Collections & Nested Generics

@type_enforced.Enforcer
def fn(
    items: list[int | float],
    mapping: dict[str, list[int]],      # Dicts require [KeyType, ValType]
    unique_ids: set[str],
    fixed_pair: tuple[str, int],        # Exact positional tuple: (str, int)
    var_tuple: tuple[int, ...],         # Variable-length tuple
) -> None:
    pass

Custom Classes & Subclass Inheritance

By default, subclasses pass type validation (e.g. Bar() satisfies Foo if class Bar(Foo)):

class Animal: pass
class Dog(Animal): pass
class Vehicle: pass

@type_enforced.Enforcer
def feed(animal: Animal) -> None:
    pass

feed(Animal())  # OK
feed(Dog())     # OK (subclasses allowed)
feed(Vehicle()) # Raises TypeError

To enforce uninitialized class objects (the class itself, rather than an instance), use type[Animal] (or typing.Type[Animal]):

@type_enforced.Enforcer
def make_instance(cls: type[Animal]) -> Animal:
    return cls()

Literals & Special Types

from typing import Literal, Callable, Sized, Any

@type_enforced.Enforcer
def fn(
    mode: Literal["read", "write"],        # Value check: must equal "read" or "write"
    handler: Callable,                     # Functions, methods, generators
    container: Sized,                      # list, dict, set, str, tuple, bytes, etc.
    wildcard: Any,                         # Permissive bypass
) -> None:
    pass
  • Stacking Literals: Literals combine with unions using OR logic (int | Literal['auto'] allows any int or the literal string 'auto').

Value Validation with Constraints

type_enforced allows post-type-check value constraints directly in type annotations.

Built-in Constraint

Validate bounds, numeric comparisons, string patterns (regex), and inclusion/exclusion:

import type_enforced
from type_enforced.utils import Constraint

@type_enforced.Enforcer
def set_score(
    score: int | Constraint(ge=0, le=100),
    code: str | Constraint(pattern=r"^[A-Z]{3}[0-9]{4}$"),
) -> bool:
    return True

set_score(85, "ABC1234")    # Passes
set_score(105, "ABC1234")   # Raises TypeError (Constraint `Less Than Or Equal To (100)` not met)
set_score(85, "invalid")    # Raises TypeError (Constraint `Regex Pattern Match` not met)

Available Constraint parameters:

  • gt, lt, ge, le, eq, ne (numeric / comparison bounds)
  • pattern (regular expression string match)
  • includes, excludes (membership checks)

Custom GenericConstraint

Write arbitrary validation logic using custom predicates:

import type_enforced
from type_enforced.utils import GenericConstraint

RGBColor = str | GenericConstraint({
    "valid_hex_color": lambda c: c.startswith("#") and len(c) in (4, 7)
})

@type_enforced.Enforcer
def render(color: RGBColor) -> None:
    pass

render("#ffffff")  # Passes
render("red")      # Raises TypeError (Constraint `valid_hex_color` not met)

Note: Constraints are evaluated after type checking. Constraints stack with unions: int | Constraint(ge=0) | Constraint(le=10).


Configuration Reference

Both @Enforcer and ModuleEnforcer accept the following configuration arguments:

Parameter Type Default Description
enabled bool True Toggle enforcement. Set False to bypass type checks (useful for production vs. debugging or per-method overrides).
strict bool True When True, raises TypeError on mismatch. When False, logs a warning to the console instead of raising.
clean_traceback bool True Filters internal type_enforced stack frames so unhandled tracebacks point directly to user code (see note below).
iterable_sample_pct int or str 100 Sampling mode or percentage (0–100) of iterable items to validate. 'first' checks the first item, 'last' checks the last item, 'log' checks a sample of ceil(log2(n)) items, 0 checks 1 random item, and 1..100 checks the specified percentage (rounding up). 100 validates all elements.
only_typed bool False When True, raises an exception upon decoration if any parameter or return value lacks a type hint.
submodules (ModuleEnforcer only) bool True Recursively enforces all sub-packages/sub-modules in the same namespace.

Configuration Options in Depth

1. Strict Typing Mode (only_typed=True)

To catch unannotated parameters or missing return annotations across your codebase, enable only_typed=True. This raises a TypeError at definition time if any parameter (excluding self/cls) or the return type lacks an annotation:

import type_enforced

@type_enforced.Enforcer(only_typed=True)
def calculate(a: int, b: int) -> int:
    return a + b

# Missing annotation on parameter `b` or missing return annotation raises immediately:
@type_enforced.Enforcer(only_typed=True)
def invalid_fn(a: int, b):
    return a
# TypeError: TypeEnforced Exception (invalid_fn): Untyped variable `b` found in function/method `invalid_fn`.

2. Warning Mode (strict=False)

Print warnings to the console instead of raising exceptions (useful for gradual adoption or debugging without breaking execution):

@type_enforced.Enforcer(strict=False)
def lenient_fn(x: int) -> int:
    return x

lenient_fn("not_an_int")
# Logs: TypeEnforced Warning (lenient_fn): Type mismatch for typed variable `x`...
# Returns "not_an_int" without raising an exception.

3. Clean Tracebacks (clean_traceback=True)

By default, clean_traceback=True temporarily hooks sys.excepthook when a type exception is raised, stripping internal type_enforced library frames so that unhandled script tracebacks point directly to the line of user code that caused the issue.

Note on Interactive Terminals / REPLs: In interactive environments (such as the Python REPL / PyREPL, IPython, or Jupyter notebooks), the shell wraps execution in an internal try...except loop and catches exceptions before they reach sys.excepthook. Consequently, interactive terminal sessions will still display the full traceback.

4. Sampled Validation (iterable_sample_pct)

For large or performance-critical collections, configure sampling instead of full iteration:

  • 'first': Validates the first element in O(1) time (runs up to 2x faster than Beartype).
  • 'last': Validates the last element in O(1) time.
  • 'log': Validates a sample of ceil(log2(n)) items across the collection.
  • 0: Validates one element chosen at random.
  • 1..100 (int): Validates the specified percentage of items (rounding up).
@type_enforced.Enforcer(iterable_sample_pct="first")
def fast_check(items: list[int]) -> int:
    return len(items)

fast_check([1, 2, 3])           # OK
fast_check(["bad_first", 2, 3])  # Raises TypeError

Contributing

Contributions are welcome!

Development Setup

We use uv for dependency management and testing in a Unix-based environment (Linux, macOS, or WSL2 on Windows).

# Clone the repository
git clone https://github.com/connor-makowski/type_enforced.git
cd type_enforced

# Install dev dependencies
uv sync --extra dev

Development Commands

Command Description
uv run pytest Run tests in local environment
uv run pytest -v Run tests with verbose output
uv run nox Run test suite across Python 3.11, 3.12, 3.13, 3.14
uv run nox -s tests-3.14 Run test suite on a specific Python version
uv run python utils/prettify.py Auto-format with autoflake and black (80 col)

Guidelines

  1. Fork the repo and create your branch from main.
  2. Ensure all tests pass across versions (uv run nox).
  3. Format code before committing (uv run python utils/prettify.py).
  4. Keep commits atomic and clearly described.
  5. Submit a pull request.

Academic Citation

If you use type_enforced in academic research, please cite our JOSS paper:

@article{Makowski2026,
  doi = {10.21105/joss.08832},
  url = {https://doi.org/10.21105/joss.08832},
  year = {2026},
  publisher = {The Open Journal},
  volume = {11},
  number = {118},
  pages = {8832},
  author = {Connor Makowski},
  title = {type_enforced: A pure Python runtime type enforcer},
  journal = {Journal of Open Source Software}
}

License

Distributed under the MIT License. See LICENSE for details.

Release files for type-enforced 2.8.1

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

Source distribution (sdist)

Source distribution for type-enforced 2.8.1
File Size Uploaded
type_enforced-2.8.1.tar.gz 52.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for type-enforced 2.8.1
File Interpreter ABI Platform
type_enforced-2.8.1-py3-none-any.whl Python 3 none any Details

Total release size: 85.8 kB

Release files / type_enforced-2.8.1.tar.gz

Download URL type_enforced-2.8.1.tar.gz
Size 52.2 kB
Tags Source
SHA-256 checksum
How to use checksums
6a61f0a65912999683f55643f30520e6ca4567e54bb88773bf023f25a8a8e644
BLAKE2b-256 checksum
How to use checksums
c57b6e8ace4b5cdfa414842fd71b3cdf4db2566d936f2f05826d1984d8f0c178
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.5

Release files / type_enforced-2.8.1-py3-none-any.whl

Download URL type_enforced-2.8.1-py3-none-any.whl
Size 33.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bc0cbfbf1c9c9fd4d83913b1c6f7a593a7adc9d0314dff9a679a47ec1fef145f
BLAKE2b-256 checksum
How to use checksums
028df4640fd42443cd1a80ec3654b00255edda014e119ef2a6142c0b27733609
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.5

Release history Release notifications | RSS feed

2.12.0

1 release file

2.11.0

1 release file

2.10.1

1 release file

2.10.0

1 release file

2.9.0

1 release file

This release

2.8.1 This release

2 release files

2.8.0

2 release files

2.7.0

2 release files

2.6.0

2 release files

2.5.0

2 release files

2.4.0

2 release files

2.3.0

2 release files

2.2.3

2 release files

2.2.2

2 release files

2.2.1

2 release files

2.2.0

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.10.1

2 release files

1.10.0

2 release files

1.9.0

2 release files

1.8.1

2 release files

1.8.0

2 release files

1.7.0

2 release files

1.6.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.0.16

1 release file

0.0.15

1 release file

0.0.14

1 release file

0.0.13

1 release file

0.0.12

1 release file

0.0.11

1 release file

0.0.10

1 release file

0.0.9

1 release file

0.0.8

1 release file

0.0.7

1 release file

0.0.6

1 release file

0.0.5

1 release file

0.0.4

1 release file

0.0.3

1 release file

0.0.2

1 release file

0.0.1

1 release file

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