Skip to content

PrivatePandaCO/TypeKeeper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

typekeeper

Lightweight, dependency-free runtime argument validation for Python via a single decorator. See Changelog.md for version history.

Overview

typekeeper instruments your functions with comprehensive runtime checks through one decorator, @validate_args. With no code changes beyond the decorator, you get:

  • Type checking against rich typing annotations.
  • Warnings for mismatched or mutable default values.
  • Inclusive numeric-range and length-range constraints via a tiny spec string, including nested-path targeting with : and wildcards.
  • Recursive _validate() traversal of nested data structures.
  • Precise source locations (file + line) for every warning, both at decoration time and call time.
  • Global, per-decorator, and contextual on/off switches.

Features

  • Type validation — recursively checks the common typing forms: built-in generics (list[int], dict[str, float]), Optional, Union / PEP 604 unions, Literal, Type[T] / type[T], Annotated, Final, NewType, TypeVar (constraints and bounds), Callable (callability and positional arity), TypedDict (structural dict check), Protocol (structural attribute check for both @runtime_checkable and plain protocols), Self, and abstract base classes such as Sequence, Mapping, and Iterable. Unresolved string forward references are treated as unchecked; ForwardRef objects are resolved against the typing namespace when possible.
  • Default-value checks — warns when a default does not match its annotation, and detects mutable defaults (suppressible per decorator via ignore_defaults=True).
  • Range & length specs — pass constraints; each spec is name=token1,token2,... where a token is either a single number N or an inclusive range min-max. Whitespace around the inner - is tolerated. Use : to traverse nested mappings and sequences, with integer segments selecting sequence elements (negative indices follow normal Python rules). Wildcards (* or a trailing :) match every element under that point; later specs override earlier ones for the same target. Missing mapping keys yield no matches (no spurious warnings).
  • Recursive _validate() — every argument is walked. Any object with a callable _validate() is invoked; falsy returns or raised exceptions are reported with the path inside the original structure. Mappings (including UserDict / MappingProxyType) are recursed via collections.abc.Mapping. Iterators and generators are never consumed by validation.
  • Context-aware warnings — messages embed both the decoration location (file:line) and the call site.
  • Global controlset_arg_checks(False) disables decoration entirely; set_stop_on_error(True) turns every warning into a ValueError; suspended_arg_checks() is a context manager that switches checks off inside a with block (overhead drops to under half a microsecond per call).

Installation

pip install typekeeper

Development

pip install -e ".[dev]"
python -m pytest tests/ --cov=typekeeper

The test suite covers 100% of typekeeper/typekeeper.py and exercises a broad slice of typing features end-to-end.

Configuration

API Effect
set_arg_checks(enabled: bool) Turn all argument checking on/off globally. Disabling at decoration time skips wrapping entirely (zero overhead).
set_stop_on_error(stop: bool) When True, every validation failure raises ValueError instead of warning.
suspended_arg_checks() Context manager that disables checks within a with block (decorator stays in place).

API Reference

validate_args

def validate_args(
    _func=None,
    constraints: str | None = None,
    *,
    ignore_defaults: bool = False,
    stop_on_error: bool | None = None,
) -> Callable: ...
  • constraints — numeric/length spec string covering both numeric ranges and sequence-length ranges (see syntax below).
  • ignore_defaults — skip the mutable-default warning if True.
  • stop_on_error — per-decorator override of the global set_stop_on_error flag; warnings from this decorator raise ValueError regardless of the global setting.

Constraint syntax — a semicolon-separated list:

name=token1,token2,...

Each token is N (exact match) or min-max (inclusive range). Nested targets use : after the name: users:tables:headers=100, rows:0:score=0-100, data:*=1-5. Whitespace inside ranges is tolerated (1 - 3 parses identically to 1-3).

Inside path segments, the following backslash escapes are honoured:

Escape Meaning
\: Literal : inside a segment.
\\ Literal backslash.
\* Literal * segment (no wildcard expansion).
\! Literal empty segment — addresses dict keys that are literally "" without firing the wildcard branch.

A bare empty segment (e.g. trailing :) or * still means every entry at this level; specs whose first segment is empty (":x=1-3") emit a parse-time warning instead of being silently dropped.

Examples

Full runnable scripts live in examples/. Each script prints its own validation overhead via examples/_bench.py.

File Topic
basic.py Type + default checks.
range_specs.py Numeric and length ranges.
path_indices.py Integer index segments (incl. negatives).
nested_override.py Wildcards and spec overrides.
custom_validate.py _validate() recursion + mutable defaults.
complex_typing.py Union, Literal, Callable (arity), TypeVar, NewType, Annotated.
typeddicts.py Structural TypedDict checks (total=True / False).
protocols.py @runtime_checkable and plain Protocol (structural attribute check).
forward_refs.py from __future__ import annotations and forward references.
globals_and_context.py set_arg_checks, set_stop_on_error, suspended_arg_checks.
# Snapshot of basic.py
from typekeeper import validate_args

@validate_args()
def greet(name: str, times: int = 1) -> str:
    return " ".join([name] * times)

greet("Alice")
greet(42)
#  UserWarning: Arg 'name' to '<function greet ...>' mismatches <class 'str'>;
#  expected <class 'str'>, got <class 'int'>

Performance

Run tools/profile.py to reproduce these numbers. They are representative of a single CPython 3.14 run on a desktop x86-64 machine (50,000 iterations per scenario, after a warm-up burst):

Scenario Bare (µs/call) Checked (µs/call) Suspended (µs/call) Overhead (µs/call)
simple (x: int, y: str) 0.08 10.9 0.19 10.8
nested_generic (list[dict[str, int]], 3 dicts) 0.35 39.5 0.66 39.1
constraints (numeric + length spec) 0.07 23.0 0.18 22.9
path_spec (rows:*:score=0-100, 5 rows) 0.05 60.6 0.25 60.6
varargs (*values: int, 6 args) 0.07 20.6 0.53 20.6
custom_validate (8-element list with _validate) 0.05 25.3 0.21 25.2

Run it yourself:

python tools/profile.py --iterations 50000          # scenario benchmark
python tools/profile.py --cprofile                  # cProfile breakdown
python tools/profile.py --iterations 50000 --json results.json

Key takeaways:

  • The decorator itself (suspended mode) costs well under 1 µs per call (typically 0.2 µs for fixed-arity signatures).
  • Scalar arguments add roughly 10 µs per call for type checking.
  • Per-element checking dominates the cost on collection-shaped signatures; expect ~5–10 µs / leaf in the worst case.
  • Path-spec wildcards walk every leaf — prefer indexed paths (rows:0:v=...) when you only care about a few cells.
  • The _recursive_validate() walk runs on every argument; if your hot path passes large structures and you don't need it, hoist the validated call out of the loop or wrap it in with suspended_arg_checks(): ....

Contributing

Contributions are welcome:

  1. Fork the repository.
  2. Create a feature branch.
  3. Run python -m pytest tests/ --cov=typekeeper (target stays at 100%).
  4. Submit a pull request.

License

MIT License.

Author

Parth Mittalparth@privatepanda.co

About

An extensive drop-in argument validator for python.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages