Skip to main content

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, PEP 695 type aliases (TypeAliasType), and abstract base classes such as Sequence, Mapping, and Iterable (mutable variants correctly reject immutable values). 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 control — set_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). Suspension is thread-/task-local, so it never disables checks running concurrently in other threads.

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); thread-/task-local.

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 Mittal — parth@privatepanda.co

Metadata

Release files for typekeeper 0.0.5

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

Source distribution (sdist)

Source distribution for typekeeper 0.0.5
File Size Uploaded
typekeeper-0.0.5.tar.gz 33.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for typekeeper 0.0.5
File Interpreter ABI Platform
typekeeper-0.0.5-py3-none-any.whl Python 3 none any Details

Total release size: 50.4 kB

Release files / typekeeper-0.0.5.tar.gz

Download URL typekeeper-0.0.5.tar.gz
Size 33.9 kB
Tags Source
SHA-256 checksum
How to use checksums
928d84b47c6f7ddf60145543f944862e5a81544f5204175b244596debb5eddb0
BLAKE2b-256 checksum
How to use checksums
3911c638620b9a586e9c0e7f284172dcb01f6fb9ac00a6242efded0449c805fa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.4

Release files / typekeeper-0.0.5-py3-none-any.whl

Download URL typekeeper-0.0.5-py3-none-any.whl
Size 16.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
21f32e5a0187c1c0e55371808fa859ef5615c1492dc79a146931e473f3373a6a
BLAKE2b-256 checksum
How to use checksums
219544f154caa71b7daa24e945063d4084121a946385e99234ee3b242e66c45f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.4

Release history Release notifications | RSS feed

This release

0.0.5 This release

2 release files

0.0.4

2 release files

0.0.2

2 release files

0.0.1

2 release files

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