Fast runtime type enforcement for Python 3.11+ type annotations. Zero dependencies and uncompromising performance.
Python
64
317 commits
updated Sep 30, 2026
Fast where it counts, thorough where it matters. Runtime validation for Python type annotations. Zero dependencies and uncompromising performance.
import type_enforced
# 1. Complete validation
@type_enforced.Enforcer
def greet(name: list[str], repeat: int = 1) -> str:
return f"Hello {', '.join(name)}!" * repeat
greet(["Alice"], 2) # Returns "Hello Alice!Hello Alice!"
greet(["Alice"], "twice") # Raises TypeError at runtime!
# 2. Fast O(1) validation (does not check every item in passed collections)
@type_enforced.FastEnforcer
def process_tags(tags: list[str]) -> int:
return len(tags)
process_tags(["admin", "user"]) # Returns 2
process_tags([123, "user"]) # Raises TypeError (first element is checked)
Enforce an entire module (complete or fast O(1) sampled validation):
import my_package
import type_enforced
# Enforce all functions and classes across my_package
type_enforced.ModuleEnforcer(my_package)
# Or for fast O(1) sampled validation across my_package:
# type_enforced.FastModuleEnforcer(my_package)
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:
type_enforced eliminates this compromise:
list[dict[str, int]] or dicts with 10,000+ keys) by default, with zero shortcuts.iterable_sample_pct='first', 'last', 'bookend', 'bookend_plus', 'log', 0 (random pick), or a percentage. Sampled validation in type_enforced runs up to ~15x faster than Beartype.| unions, nested generics, Literals, Callables, Dataclasses, custom class inheritance, and custom validation Constraint rules.Timings represent the added differential validation time (enforced call time minus non-enforced baseline call time) in nanoseconds (ns), averaged over 100 runs when using the C++ backend. For full benchmarks see utils/benchmark.py and benchmark.md.
| Type | Size | type_enforced (sample=1) | Beartype (sample=1) | Typeguard (sample=1) | type_enforced (100%) | Pydantic (100%) | msgspec (100%) | cattrs (100%) | Typeguard (100%) |
|---|---|---|---|---|---|---|---|---|---|
int | — | 10.5 ns | 190.8 ns | 1879.2 ns | 10.6 ns | 490.6 ns | 264.4 ns | 116.1 ns | 1903.7 ns |
Union[int, float] | — | 14.6 ns | 214.5 ns | 3903.9 ns | 13.3 ns | 536.8 ns | 400.0 ns | 426.0 ns | 3905.0 ns |
str | — | 10.5 ns | 198.2 ns | 1885.0 ns | 10.6 ns | 489.1 ns | 263.5 ns | 119.1 ns | 1910.0 ns |
list[int] | 1 000 items | 17.4 ns | 335.2 ns | 3221.6 ns | 455.0 ns | 11312.0 ns | 5169.1 ns | 48737.6 ns | 1052411.9 ns |
dict[str, int] | 1 000 keys | 39.7 ns | 350.6 ns | 4496.5 ns | 3084.8 ns | 40401.9 ns | 26886.3 ns | 68340.9 ns | 2086004.8 ns |
list[list[int]] | 10 x 100 items | 20.5 ns | 376.4 ns | 4438.1 ns | 363.7 ns | 11703.8 ns | 5985.2 ns | 48919.3 ns | 1062224.5 ns |
dict[str, list[int]] | 10 x 100 items | 42.4 ns | 453.5 ns | 5793.9 ns | 412.2 ns | 12182.7 ns | 6494.8 ns | 49395.8 ns | 1072765.8 ns |
list[dict[str, int]] | 10 x 100 items | 42.6 ns | 444.6 ns | 5728.2 ns | 3494.7 ns | 39740.2 ns | 26101.5 ns | 66918.9 ns | 2145857.3 ns |
Sampled Validation: When 1 sample validation is acceptable,
type_enforced.FastEnforceris up to ~15x faster than Beartype.
Full Validation: When full validation is required,
type_enforced.Enforceris up to ~40x faster than Pydantic on scalars and up to ~20x faster on larger data structures.
Install via pip:
pip install type_enforced
Or using uv:
uv add type_enforced
Python 3.11+
Zero Runtime Dependencies: Self-contained package with zero external runtime dependencies.
C++ Acceleration: If available, type_enforced leverages high-performance C++ validators via nanobind.
Pure Python Fallback: If compiling from source on a system without a C++ compiler, type_enforced automatically falls back to a pure-Python engine.
Force Pure Python Fallback: To explicitly skip C++ compilation and force pure Python mode:
uv (in pyproject.toml):
[tool.uv]
no-binary-package = ["type-enforced"]
config-settings-package = { type-enforced = { "cmake.define.SKIP_CPP_BUILD" = "ON" } }
pip (in pyproject.toml when building from source):
[tool.scikit-build.cmake.define]
SKIP_CPP_BUILD = "ON"
pip (in requirements.txt):
type_enforced --config-settings=cmake.define.SKIP_CPP_BUILD=ON --no-binary type_enforced
pip (CLI):
pip install type_enforced --no-binary type_enforced -Ccmake.define.SKIP_CPP_BUILD=ON
(Or set SKBUILD_CMAKE_ARGS="-DSKIP_CPP_BUILD=ON" and PIP_NO_BINARY="type_enforced" in your environment)
Verify C++ Acceleration Status: Check whether C++ acceleration is active in the current environment:
import type_enforced
print(type_enforced.has_cpp()) # True if C++ acceleration is active, False for pure Python
For older Python versions, pin to legacy releases:
pip install "type_enforced<=1.10.2"pip install "type_enforced<=1.9.0"pip install "type_enforced==0.0.16"Apply @type_enforced.Enforcer or @type_enforced.FastEnforcer 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.
Decorating a class with @type_enforced.Enforcer or @type_enforced.FastEnforcer 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
ModuleEnforcer or FastModuleEnforcer)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() # Complete validation across module
# Or for fast O(1) sampled validation across the module:
# type_enforced.FastModuleEnforcer()
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)
# Or: type_enforced.FastModuleEnforcer(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.
type_enforced supports all standard Python 3.11+ typing constructs:
@type_enforced.Enforcer
def fn(
a: int,
b: str | float, # Standard union syntax
c: int | None = None, # Optional syntax
) -> None:
pass
@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
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()
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
int | Literal['auto'] allows any int or the literal string 'auto').type_enforced comprehensively supports modern typing features from recent Python PEPs:
from typing import (
Callable,
LiteralString,
Never,
NewType,
NoReturn,
Self,
TypeGuard,
TypeIs,
TypeVar,
TypedDict,
)
# 1. PEP 673: typing.Self
class Builder:
@type_enforced.Enforcer
def set_name(self, name: str) -> Self:
self.name = name
return self
# 2. PEP 589: typing.TypedDict (validates required keys & field types)
class UserPayload(TypedDict):
id: int
name: str
@type_enforced.Enforcer
def create_user(payload: UserPayload) -> str:
return payload["name"]
# 3. PEP 484: typing.NewType
UserId = NewType("UserId", int)
@type_enforced.Enforcer
def get_user(user_id: UserId) -> None:
pass
# 4. Subscripted Callables (PEP 484 & PEP 612)
@type_enforced.Enforcer
def apply_handler(callback: Callable[[int, str], bool]) -> None:
pass
# 5. PEP 675: typing.LiteralString
@type_enforced.Enforcer
def run_query(sql: LiteralString) -> None:
pass
# 6. PEP 484 / PEP 654: NoReturn and Never
@type_enforced.Enforcer
def terminate() -> NoReturn:
raise SystemExit(0)
# 7. PEP 647 & PEP 742: TypeGuard and TypeIs
@type_enforced.Enforcer
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
# 8. TypeVar, ParamSpec, TypeVarTuple & PEP 695 (Python 3.12+)
T = TypeVar("T", bound=int | float)
@type_enforced.Enforcer
def scale(val: T, factor: float) -> float:
return val * factor
Unions of collection types are evaluated per-variant, enforcing that each container strictly satisfies one schema rather than allowing mixed elements:
@type_enforced.Enforcer
def process_data(
coords: tuple[int, str] | tuple[str, int],
lookup: dict[str, list[int]] | dict[str, int],
tags: list[int] | list[str],
) -> None:
pass
# Distinct collection schemas match:
process_data((1, "north"), {"a": [1, 2]}, [1, 2, 3]) # OK
process_data(("north", 1), {"a": 10}, ["a", "b"]) # OK
# Mixed invalid structures fail:
process_data((1, 1), {"a": 10}, [1, 2]) # Raises TypeError for coords
process_data(
(1, "north"), {"a": 1, "b": [2]}, [1, 2]
) # Raises TypeError for lookup
process_data((1, "north"), {"a": 10}, [1, "two"]) # Raises TypeError for tags
*args and **kwargs are fully supported with clear, indexed error messages:
@type_enforced.Enforcer
def configure(*flags: str, **settings: int | bool) -> None:
pass
configure("verbose", "debug", timeout=30, dry_run=True) # OK
configure("verbose", 123) # Raises TypeError: Type mismatch for typed variable `flags[1]`
configure(timeout="30s") # Raises TypeError: Type mismatch for typed variable `settings['timeout']`
Sized (e.g. Sized[int] — use Sized without inner type arguments)type_enforced allows post-type-check value constraints directly in type annotations.
ConstraintValidate 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)GenericConstraintWrite 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).
@Enforcer, @FastEnforcer, ModuleEnforcer, and FastModuleEnforcer 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, float, or str | 100 ('first' for Fast*) | Sampling mode or percentage (0–100) of iterable items to validate. 'first' checks the first item, 'last' checks the last item (or first item for dicts/sets), 'bookend' checks first and last items (first 2 items for dicts/sets), 'bookend_plus' checks first, last, and a random middle item (first 2 items and 1 random item for dicts/sets), 'log' checks a sample of ceil(log2(n)) items using a pseudo-random start offset and even steps across sequences (first ceil(log2(n)) items for dicts/sets), 0 checks 1 random item, and 1..100 checks the specified percentage (rounding up) starting at a pseudo-random offset within each step interval for sequences (first $N$ items for dicts/sets). 100 validates all elements. Note: FastEnforcer and FastModuleEnforcer strictly accept 'first', 'last', 'bookend', 'bookend_plus', 'log', or 0. |
only_typed | bool | False | When True, raises an exception upon decoration if any parameter or return value lacks a type hint. |
submodules (ModuleEnforcers only) | bool | True | Recursively enforces all sub-packages/sub-modules in the same namespace. |
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`.
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.
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...exceptloop and catches exceptions before they reachsys.excepthook. Consequently, interactive terminal sessions will still display the full traceback.
FastEnforcer, FastModuleEnforcer, iterable_sample_pct)For large or performance-critical collections, use @type_enforced.FastEnforcer or configure sampling instead of full iteration:
'first' (default for FastEnforcer / FastModuleEnforcer): Validates the first element in O(1) time (runs up to ~15x faster than Beartype).'last': Validates the last element in O(1) time for indexable sequences (list, tuple). For non-indexed collections like dict and set, 'last' validates the first item to avoid reverse iteration and hash table lookup overhead.'bookend': Validates the first and last elements in O(1) time for sequences (the first 2 items for dict and set).'bookend_plus': Validates the first, last, and a random middle element in O(1) time for sequences (the first 2 items and 1 random item for dict and set).'log': For sequences (list, tuple), samples ceil(log2(n)) items by picking a Weyl pseudo-random start offset and taking even step jumps across the collection. For dict and set, validates the first ceil(log2(n)) items.0: Validates one element chosen at random.1..99 (int, Enforcer / ModuleEnforcer only): Validates the specified percentage of items (rounding up). For sequences, selects a Weyl pseudo-random start offset in [0, step - 1] and takes even step jumps across the collection, giving every index an equal probability of being checked. For dict and set, validates the first N items.100: Complete validation of all items across the collection.# Using FastEnforcer directly:
@type_enforced.FastEnforcer
def fast_check(items: list[int]) -> int:
return len(items)
fast_check([1, 2, 3]) # OK
fast_check(["bad_first", 2, 3]) # Raises TypeError
# Or configure Enforcer with a specific sample mode:
@type_enforced.Enforcer(iterable_sample_pct="last")
def check_last(items: list[int]) -> int:
return len(items)
clean_traceback=False)By default, clean_traceback=True temporarily hooks sys.excepthook to filter internal library frames for standalone scripts. In concurrent multi-threaded environments and applications using centralized error handlers, consider setting clean_traceback=False:
import type_enforced
@type_enforced.Enforcer(clean_traceback=False)
def process_request(user_id: int, tags: list[str]) -> dict:
return {"user_id": user_id, "tags": tags}
Contributions are welcome!
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
| 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.14 (C++ and pure-Python fallback) |
uv run nox -s tests-3.14 | Run test suite on a specific Python version |
uv run python utils/minibench.py | Run quick performance at a glance benchmark |
uv run python utils/cpp_vs_python_bench.py | Run C++ accelerated vs pure Python benchmark |
uv run python utils/prettify.py | Auto-format with autoflake and black (80 col) |
main.uv run nox).uv run python utils/prettify.py).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}
}
Distributed under the MIT License. See LICENSE for details.
263 followers · starred Feb 2023
Python
81.8%
C++
17.5%
Fast runtime type enforcement for Python 3.11+ type annotations. Zero dependencies and uncompromising performance.
Python
64
317 commits
updated Sep 30, 2026
Fast where it counts, thorough where it matters. Runtime validation for Python type annotations. Zero dependencies and uncompromising performance.
import type_enforced
# 1. Complete validation
@type_enforced.Enforcer
def greet(name: list[str], repeat: int = 1) -> str:
return f"Hello {', '.join(name)}!" * repeat
greet(["Alice"], 2) # Returns "Hello Alice!Hello Alice!"
greet(["Alice"], "twice") # Raises TypeError at runtime!
# 2. Fast O(1) validation (does not check every item in passed collections)
@type_enforced.FastEnforcer
def process_tags(tags: list[str]) -> int:
return len(tags)
process_tags(["admin", "user"]) # Returns 2
process_tags([123, "user"]) # Raises TypeError (first element is checked)
Enforce an entire module (complete or fast O(1) sampled validation):
import my_package
import type_enforced
# Enforce all functions and classes across my_package
type_enforced.ModuleEnforcer(my_package)
# Or for fast O(1) sampled validation across my_package:
# type_enforced.FastModuleEnforcer(my_package)
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:
type_enforced eliminates this compromise:
list[dict[str, int]] or dicts with 10,000+ keys) by default, with zero shortcuts.iterable_sample_pct='first', 'last', 'bookend', 'bookend_plus', 'log', 0 (random pick), or a percentage. Sampled validation in type_enforced runs up to ~15x faster than Beartype.| unions, nested generics, Literals, Callables, Dataclasses, custom class inheritance, and custom validation Constraint rules.Timings represent the added differential validation time (enforced call time minus non-enforced baseline call time) in nanoseconds (ns), averaged over 100 runs when using the C++ backend. For full benchmarks see utils/benchmark.py and benchmark.md.
| Type | Size | type_enforced (sample=1) | Beartype (sample=1) | Typeguard (sample=1) | type_enforced (100%) | Pydantic (100%) | msgspec (100%) | cattrs (100%) | Typeguard (100%) |
|---|---|---|---|---|---|---|---|---|---|
int | — | 10.5 ns | 190.8 ns | 1879.2 ns | 10.6 ns | 490.6 ns | 264.4 ns | 116.1 ns | 1903.7 ns |
Union[int, float] | — | 14.6 ns | 214.5 ns | 3903.9 ns | 13.3 ns | 536.8 ns | 400.0 ns | 426.0 ns | 3905.0 ns |
str | — | 10.5 ns | 198.2 ns | 1885.0 ns | 10.6 ns | 489.1 ns | 263.5 ns | 119.1 ns | 1910.0 ns |
list[int] | 1 000 items | 17.4 ns | 335.2 ns | 3221.6 ns | 455.0 ns | 11312.0 ns | 5169.1 ns | 48737.6 ns | 1052411.9 ns |
dict[str, int] | 1 000 keys | 39.7 ns | 350.6 ns | 4496.5 ns | 3084.8 ns | 40401.9 ns | 26886.3 ns | 68340.9 ns | 2086004.8 ns |
list[list[int]] | 10 x 100 items | 20.5 ns | 376.4 ns | 4438.1 ns | 363.7 ns | 11703.8 ns | 5985.2 ns | 48919.3 ns | 1062224.5 ns |
dict[str, list[int]] | 10 x 100 items | 42.4 ns | 453.5 ns | 5793.9 ns | 412.2 ns | 12182.7 ns | 6494.8 ns | 49395.8 ns | 1072765.8 ns |
list[dict[str, int]] | 10 x 100 items | 42.6 ns | 444.6 ns | 5728.2 ns | 3494.7 ns | 39740.2 ns | 26101.5 ns | 66918.9 ns | 2145857.3 ns |
Sampled Validation: When 1 sample validation is acceptable,
type_enforced.FastEnforceris up to ~15x faster than Beartype.
Full Validation: When full validation is required,
type_enforced.Enforceris up to ~40x faster than Pydantic on scalars and up to ~20x faster on larger data structures.
Install via pip:
pip install type_enforced
Or using uv:
uv add type_enforced
Python 3.11+
Zero Runtime Dependencies: Self-contained package with zero external runtime dependencies.
C++ Acceleration: If available, type_enforced leverages high-performance C++ validators via nanobind.
Pure Python Fallback: If compiling from source on a system without a C++ compiler, type_enforced automatically falls back to a pure-Python engine.
Force Pure Python Fallback: To explicitly skip C++ compilation and force pure Python mode:
uv (in pyproject.toml):
[tool.uv]
no-binary-package = ["type-enforced"]
config-settings-package = { type-enforced = { "cmake.define.SKIP_CPP_BUILD" = "ON" } }
pip (in pyproject.toml when building from source):
[tool.scikit-build.cmake.define]
SKIP_CPP_BUILD = "ON"
pip (in requirements.txt):
type_enforced --config-settings=cmake.define.SKIP_CPP_BUILD=ON --no-binary type_enforced
pip (CLI):
pip install type_enforced --no-binary type_enforced -Ccmake.define.SKIP_CPP_BUILD=ON
(Or set SKBUILD_CMAKE_ARGS="-DSKIP_CPP_BUILD=ON" and PIP_NO_BINARY="type_enforced" in your environment)
Verify C++ Acceleration Status: Check whether C++ acceleration is active in the current environment:
import type_enforced
print(type_enforced.has_cpp()) # True if C++ acceleration is active, False for pure Python
For older Python versions, pin to legacy releases:
pip install "type_enforced<=1.10.2"pip install "type_enforced<=1.9.0"pip install "type_enforced==0.0.16"Apply @type_enforced.Enforcer or @type_enforced.FastEnforcer 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.
Decorating a class with @type_enforced.Enforcer or @type_enforced.FastEnforcer 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
ModuleEnforcer or FastModuleEnforcer)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() # Complete validation across module
# Or for fast O(1) sampled validation across the module:
# type_enforced.FastModuleEnforcer()
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)
# Or: type_enforced.FastModuleEnforcer(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.
type_enforced supports all standard Python 3.11+ typing constructs:
@type_enforced.Enforcer
def fn(
a: int,
b: str | float, # Standard union syntax
c: int | None = None, # Optional syntax
) -> None:
pass
@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
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()
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
int | Literal['auto'] allows any int or the literal string 'auto').type_enforced comprehensively supports modern typing features from recent Python PEPs:
from typing import (
Callable,
LiteralString,
Never,
NewType,
NoReturn,
Self,
TypeGuard,
TypeIs,
TypeVar,
TypedDict,
)
# 1. PEP 673: typing.Self
class Builder:
@type_enforced.Enforcer
def set_name(self, name: str) -> Self:
self.name = name
return self
# 2. PEP 589: typing.TypedDict (validates required keys & field types)
class UserPayload(TypedDict):
id: int
name: str
@type_enforced.Enforcer
def create_user(payload: UserPayload) -> str:
return payload["name"]
# 3. PEP 484: typing.NewType
UserId = NewType("UserId", int)
@type_enforced.Enforcer
def get_user(user_id: UserId) -> None:
pass
# 4. Subscripted Callables (PEP 484 & PEP 612)
@type_enforced.Enforcer
def apply_handler(callback: Callable[[int, str], bool]) -> None:
pass
# 5. PEP 675: typing.LiteralString
@type_enforced.Enforcer
def run_query(sql: LiteralString) -> None:
pass
# 6. PEP 484 / PEP 654: NoReturn and Never
@type_enforced.Enforcer
def terminate() -> NoReturn:
raise SystemExit(0)
# 7. PEP 647 & PEP 742: TypeGuard and TypeIs
@type_enforced.Enforcer
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
# 8. TypeVar, ParamSpec, TypeVarTuple & PEP 695 (Python 3.12+)
T = TypeVar("T", bound=int | float)
@type_enforced.Enforcer
def scale(val: T, factor: float) -> float:
return val * factor
Unions of collection types are evaluated per-variant, enforcing that each container strictly satisfies one schema rather than allowing mixed elements:
@type_enforced.Enforcer
def process_data(
coords: tuple[int, str] | tuple[str, int],
lookup: dict[str, list[int]] | dict[str, int],
tags: list[int] | list[str],
) -> None:
pass
# Distinct collection schemas match:
process_data((1, "north"), {"a": [1, 2]}, [1, 2, 3]) # OK
process_data(("north", 1), {"a": 10}, ["a", "b"]) # OK
# Mixed invalid structures fail:
process_data((1, 1), {"a": 10}, [1, 2]) # Raises TypeError for coords
process_data(
(1, "north"), {"a": 1, "b": [2]}, [1, 2]
) # Raises TypeError for lookup
process_data((1, "north"), {"a": 10}, [1, "two"]) # Raises TypeError for tags
*args and **kwargs are fully supported with clear, indexed error messages:
@type_enforced.Enforcer
def configure(*flags: str, **settings: int | bool) -> None:
pass
configure("verbose", "debug", timeout=30, dry_run=True) # OK
configure("verbose", 123) # Raises TypeError: Type mismatch for typed variable `flags[1]`
configure(timeout="30s") # Raises TypeError: Type mismatch for typed variable `settings['timeout']`
Sized (e.g. Sized[int] — use Sized without inner type arguments)type_enforced allows post-type-check value constraints directly in type annotations.
ConstraintValidate 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)GenericConstraintWrite 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).
@Enforcer, @FastEnforcer, ModuleEnforcer, and FastModuleEnforcer 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, float, or str | 100 ('first' for Fast*) | Sampling mode or percentage (0–100) of iterable items to validate. 'first' checks the first item, 'last' checks the last item (or first item for dicts/sets), 'bookend' checks first and last items (first 2 items for dicts/sets), 'bookend_plus' checks first, last, and a random middle item (first 2 items and 1 random item for dicts/sets), 'log' checks a sample of ceil(log2(n)) items using a pseudo-random start offset and even steps across sequences (first ceil(log2(n)) items for dicts/sets), 0 checks 1 random item, and 1..100 checks the specified percentage (rounding up) starting at a pseudo-random offset within each step interval for sequences (first $N$ items for dicts/sets). 100 validates all elements. Note: FastEnforcer and FastModuleEnforcer strictly accept 'first', 'last', 'bookend', 'bookend_plus', 'log', or 0. |
only_typed | bool | False | When True, raises an exception upon decoration if any parameter or return value lacks a type hint. |
submodules (ModuleEnforcers only) | bool | True | Recursively enforces all sub-packages/sub-modules in the same namespace. |
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`.
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.
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...exceptloop and catches exceptions before they reachsys.excepthook. Consequently, interactive terminal sessions will still display the full traceback.
FastEnforcer, FastModuleEnforcer, iterable_sample_pct)For large or performance-critical collections, use @type_enforced.FastEnforcer or configure sampling instead of full iteration:
'first' (default for FastEnforcer / FastModuleEnforcer): Validates the first element in O(1) time (runs up to ~15x faster than Beartype).'last': Validates the last element in O(1) time for indexable sequences (list, tuple). For non-indexed collections like dict and set, 'last' validates the first item to avoid reverse iteration and hash table lookup overhead.'bookend': Validates the first and last elements in O(1) time for sequences (the first 2 items for dict and set).'bookend_plus': Validates the first, last, and a random middle element in O(1) time for sequences (the first 2 items and 1 random item for dict and set).'log': For sequences (list, tuple), samples ceil(log2(n)) items by picking a Weyl pseudo-random start offset and taking even step jumps across the collection. For dict and set, validates the first ceil(log2(n)) items.0: Validates one element chosen at random.1..99 (int, Enforcer / ModuleEnforcer only): Validates the specified percentage of items (rounding up). For sequences, selects a Weyl pseudo-random start offset in [0, step - 1] and takes even step jumps across the collection, giving every index an equal probability of being checked. For dict and set, validates the first N items.100: Complete validation of all items across the collection.# Using FastEnforcer directly:
@type_enforced.FastEnforcer
def fast_check(items: list[int]) -> int:
return len(items)
fast_check([1, 2, 3]) # OK
fast_check(["bad_first", 2, 3]) # Raises TypeError
# Or configure Enforcer with a specific sample mode:
@type_enforced.Enforcer(iterable_sample_pct="last")
def check_last(items: list[int]) -> int:
return len(items)
clean_traceback=False)By default, clean_traceback=True temporarily hooks sys.excepthook to filter internal library frames for standalone scripts. In concurrent multi-threaded environments and applications using centralized error handlers, consider setting clean_traceback=False:
import type_enforced
@type_enforced.Enforcer(clean_traceback=False)
def process_request(user_id: int, tags: list[str]) -> dict:
return {"user_id": user_id, "tags": tags}
Contributions are welcome!
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
| 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.14 (C++ and pure-Python fallback) |
uv run nox -s tests-3.14 | Run test suite on a specific Python version |
uv run python utils/minibench.py | Run quick performance at a glance benchmark |
uv run python utils/cpp_vs_python_bench.py | Run C++ accelerated vs pure Python benchmark |
uv run python utils/prettify.py | Auto-format with autoflake and black (80 col) |
main.uv run nox).uv run python utils/prettify.py).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}
}
Distributed under the MIT License. See LICENSE for details.
263 followers · starred Feb 2023
Python
81.8%
C++
17.5%