Skip to main content

Runtime type checking decorator for Python functions and classes

Project description

runtypecheck

Fast, pragmatic runtime type checking for Python 3.10+: a configurable @typecheck decorator (and class wrapper) featuring structural Protocol validation, constrained & bound TypeVar handling, iterable sampling, lazy iterator inspection, async support, custom validators, and a weak-reference LRU cache.


Why?

Static type checkers (mypy, pyright, pyre) are invaluable before runtime. This library adds inexpensive runtime assurance at the boundaries where static analysis may be weak: plugin entry points, notebook experiments, dynamically constructed data, tests, or external inputs. Design goals:

  • Pragmatic: unknown / future typing forms are accepted by default (configurable fallback policy).
  • Fast: caches resolved hints & origins, samples containers, and short‑circuits early.
  • Focused: only one decorator + a small config surface; no metaclass trickery.
  • Extensible: drop-in custom validators with a tiny decorator.

Quick Start

from typecheck import typecheck, TypeCheckError

@typecheck()
def greet(name: str, times: int = 1) -> str:
    return ' '.join([f'Hello {name}!'] * times)

print(greet('Alice', 2))          # OK
try:
    greet('Bob', 'x')              # type: ignore
except TypeCheckError as e:
    print('Caught:', e)

Apply to a class to wrap all public methods (plus __init__ / __call__).

@typecheck()
class Calc:
    def add(self, a: int, b: int) -> int: return a + b

Calc().add(1, 2)

Feature Matrix

Category Support Summary
Primitives / builtins Standard isinstance semantics
Container generics list, tuple (fixed & variadic), set, frozenset, dict, deque + ABCs (Sequence, Mapping, Iterable, Iterator)
Collections sampling Validates up to N elements (configurable) unless deep mode
Unions & Optional Full branch validation with aggregated mismatch context
Literal[...] Membership check
Callable Light structural check: positional arity & simple annotation compatibility
Type[Cls] Class identity / subclass acceptance
Final / ClassVar Inner type validated
Structural Protocol Attributes & method signature compatibility (positional params & annotations + return)
TypeVar Constraint / bound enforcement + per-call consistent binding
Annotated[T, ...] Treated as T (metadata ignored)
TypedDict Required keys + per-key value validation; extra keys allowed (PEP 589 semantics)
NewType Validated against its underlying supertype
Never Always error if a runtime value is supplied
NoReturn Accepted for parameter context (enforced on returns elsewhere)
LiteralString (3.11+) Treated as str (best effort)
TypeGuard[T] Ensures runtime bool result
Forward refs Permissive or strict (config)
Async functions Wrapper preserves async and validates awaited result
Lazy iterables Non-length iterables sampled via itertools.tee
Deep vs sample deep=True overrides sampling; otherwise first N elements
Custom validators @register_validator(cls) mapping exact type -> predicate
Runtime disable TYPECHECK_DISABLED=1 env var skips decoration logic

Unsupported / unrecognized constructs (e.g., advanced future typing forms) fall back to acceptance unless config.fallback_policy is set to warn or error.


Installation

pip install runtypecheck

Python 3.10–3.13 (tested). Zero runtime dependencies.


Usage Examples

Collections & Sampling

from typecheck import typecheck, TypeCheckError

@typecheck(sample=3)   # only first 3 elements of large list validated
def head_sum(values: list[int]) -> int:
    return sum(values[:3])

@typecheck(deep=True)  # validate every element
def full_sum(values: list[int]) -> int:
    return sum(values)

head_sum([1,2,3,'x',5])         # OK (sampling hides later mismatch)  # type: ignore
try:
    full_sum([1,2,3,'x',5])     # type: ignore
except TypeCheckError: pass

Protocols & TypeVars

from typing import Protocol, TypeVar
from typecheck import typecheck, TypeCheckError

class SupportsClose(Protocol):
    def close(self) -> None: ...

@typecheck()
def shutdown(r: SupportsClose) -> None: r.close()

T = TypeVar('T', int, str)
@typecheck()
def echo(x: T) -> T: return x

Custom Validator

from typecheck import register_validator, typecheck, TypeCheckError

class PositiveInt(int):
    pass

@register_validator(PositiveInt)
def _validate_positive(v, t): return isinstance(v, int) and v >= 0

@typecheck()
def square(x: PositiveInt) -> int: return x * x

Async

import asyncio
from typecheck import typecheck

@typecheck()
async def fetch(n: int) -> int:
    return n * 2

asyncio.run(fetch(5))

Strict Modes & Method Selection

Wrap only selected methods or ignore specific ones:

from typecheck import typecheck

@typecheck(include=["process", "finalize"], exclude=["finalize"])  # only "process" gets wrapped
class Job:
    def process(self, x: int) -> int: return x
    def finalize(self, x: int) -> int: return x  # excluded
    def helper(self, x: int) -> int: return x  # not in include list

class Service:
    @typecheck(ignore=True)  # marker to skip when class decorated
    def fast_path(self, x: int) -> int: return x
    @typecheck()
    def strict_path(self, x: int) -> int: return x

Service = typecheck()(Service)

Parameters:

  • include=[...]: Only listed methods (plus __init__ / __call__).
  • exclude=[...]: Remove methods after inclusion filtering.
  • Per-method @typecheck(ignore=True): Skip even under class decorator.
from typecheck import typecheck, config

config.strict_mode = True          # missing parameter annotations raise
config.strict_return_mode = True   # missing return annotations raise

Configuration (typecheck.config)

Attribute Default Effect
sample_size 5 Default element sample for collections / iterables
strict_mode False Enforce all parameters annotated
strict_return_mode False Enforce return annotation presence (independent of strict_mode)
deep_checking False If True, decorator defaults to deep validation when deep not passed
lazy_iterable_validation True Sample first N elements of single‑pass iterables via itertools.tee
fallback_policy "silent" Behavior for unsupported constructs: silent / warn / error
forward_ref_policy "permissive" Unresolved forward refs: permissive accept or strict error

Per‑call overrides: @typecheck(sample=10), @typecheck(deep=True), @typecheck(strict=True), etc.

Reset to defaults:

from typecheck import config
config.reset()

Fallback Policy

If a construct is unrecognized, _check_type accepts it by default (policy silent). Change behavior:

from typecheck import config
config.set_fallback_policy("warn")   # or "error"

warn emits a RuntimeWarning; error raises immediately.


Error Messages

Errors raise TypeCheckError with a concise diagnostic:

Type mismatch for parameter 'age' in function 'greet': expected int, got str ('twenty-five')

Return mismatches use: Return value type mismatch in function 'func': expected list[int], got dict (...).


Performance Notes

  • Cached: resolved get_type_hints + origin/args via weak LRU caches.
  • Sampling: limits deep traversal cost for large structures & streams.
  • Iterables: lazy path avoids exhausting one‑shot generators.
  • Overhead on simple primitive calls is typically a handful of microseconds (implementation detail; measure in your environment).

Disable entirely with an environment variable:

TYPECHECK_DISABLED=1 python your_app.py

Custom Validators API

from typecheck import register_validator

@register_validator(MyType)
def validate(value, expected_type) -> bool:
    # return True / False or raise TypeCheckError for custom message
    ...

Validators run before built‑in generic origin handlers.


Weak LRU Cache Utility

typecheck.weak_lru.lru_cache(maxsize=..., typed=False) behaves like functools.lru_cache but stores per‑instance caches for methods in a WeakKeyDictionary so instances can be GC’d.

from typecheck import weak_lru

@weak_lru.lru_cache(maxsize=256)
def fib(n: int) -> int:
    return n if n < 2 else fib(n-1)+fib(n-2)

Use .cache_info() / .cache_clear() same as stdlib.


Advanced Topics

  • Deep vs Sampled: @typecheck(deep=True) enforces full traversal; otherwise first sample_size (config or decorator arg) elements validated.
  • Lazy Iterables: When lazy_iterable_validation is True and object lacks __len__, the library samples via itertools.tee without consuming the original iterator.
  • Protocol Enumeration: Methods, properties, classmethods, staticmethods, and annotated attributes all counted as required members.
  • TypeVar Binding: A fresh context per function call enforces consistent multi-parameter binding (subtype-compatible reuse accepted).
  • TypeGuard: Treated as bool sanity gate.

Testing

The project ships with a comprehensive pytest suite (async, protocols, lazy iterables, custom validators, strict returns, weak LRU). Run:

pytest --cov=src/typecheck --cov-report=term-missing

Roadmap (Abridged)

  • Finer-grained Callable variance & keyword kind checking
  • Optional stricter Protocol variance rules
  • Configurable error formatter hook
  • Extended TypedDict total / optional key strictness flags
  • Richer metadata usage for Annotated

Packaging & Type Information

The distribution includes a py.typed marker so static type checkers (mypy, pyright) can consume inline type hints.

Supported Python versions: 3.10, 3.11, 3.12, 3.13.

Partially handled (best-effort) constructs: LiteralString (treated as str), Annotated (metadata ignored). Unsupported advanced forms like ParamSpec, Concatenate, Unpack, Required / NotRequired, Self currently fall back per the fallback policy.


License

AGPL-3.0-or-later. See LICENSE.


Cheat Sheet

Want Use
Enforce parameter annotations globally config.strict_mode = True
Enforce return annotations too config.strict_return_mode = True
Disable sampling for a call @typecheck(deep=True)
Increase sampling globally config.set_sample_size(10)
Custom validator @register_validator(MyType)
Skip runtime cost (env) TYPECHECK_DISABLED=1
Validate generator lazily leave config.lazy_iterable_validation = True
Strict on one function only @typecheck(strict=True)

Happy checking!

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

runtypecheck-0.1.1.tar.gz (47.8 kB view details)

Uploaded Source

Built Distribution

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

runtypecheck-0.1.1-py3-none-any.whl (36.4 kB view details)

Uploaded Python 3

File details

Details for the file runtypecheck-0.1.1.tar.gz.

File metadata

  • Download URL: runtypecheck-0.1.1.tar.gz
  • Upload date:
  • Size: 47.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for runtypecheck-0.1.1.tar.gz
Algorithm Hash digest
SHA256 dcd622198c105a42b0889284d0ac0ed01e9e98e26f863ee72e4aeaef431efda3
MD5 311d16c05c36ddffcb141deefc00145f
BLAKE2b-256 1c07f778f6ec181db9fb4f1aae09961c3923e9e7c6389336095963f85b02b15d

See more details on using hashes here.

Provenance

The following attestation bundles were made for runtypecheck-0.1.1.tar.gz:

Publisher: release.yml on okleinke/runtypecheck

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

File details

Details for the file runtypecheck-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: runtypecheck-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 36.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for runtypecheck-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 38fae083dfcb93617f8578e6f49e7d873ab9d48358d3e7ff2131f5045486efc4
MD5 16b653cbe7ddc47424178251fc589397
BLAKE2b-256 c73d0a1e06aa5579a0f414d0431cdbb202d29f350eb3e93c5259a6cbe67299af

See more details on using hashes here.

Provenance

The following attestation bundles were made for runtypecheck-0.1.1-py3-none-any.whl:

Publisher: release.yml on okleinke/runtypecheck

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