Runtime type checking with typeguard: @typechecked, check_type and generics: working versions (tested 2026-10-18)
Enforce annotations at runtime with typeguard: @typechecked functions, standalone check_type, TypeCheckError messages and bounded TypeVars, plus the FIRST_ITEM container gotcha.
passed6 passed · 0 failed2026-10-18 03:57 UTC- Status
- passed
- Last passed
- 2026-10-18 03:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
typeguard@4.6.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 39095370682
The code
Exactly the files that passed. Your agent can pull them with get_recipe("typeguard-runtime-typecheck") (install), or see them on GitHub.
requirements.txt
typeguard==4.6.0 pytest==9.1.1
checks.py
from collections.abc import Sequence
from numbers import Number
from typing import TypeVar
from typeguard import CollectionCheckStrategy, check_type, typechecked
T = TypeVar("T", bound=Number)
U = TypeVar("U")
@typechecked
def clamp(value: int, low: int, high: int) -> int:
return max(low, min(high, value))
@typechecked
def mean(values: Sequence[float]) -> float:
return sum(values) / len(values)
@typechecked
def checked_sum(a: T, b: T) -> T:
return a + b
@typechecked
def pair(a: U, b: U) -> tuple[U, U]:
return a, b
def parse_ids(raw: object) -> list[int]:
return check_type(
raw, list[int], collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS
)
tests/test_checks.py
import pytest
from typeguard import CollectionCheckStrategy, TypeCheckError, check_type, typechecked
from checks import checked_sum, clamp, mean, pair, parse_ids
def test_argument_types_are_checked():
assert clamp(5, 0, 10) == 5
with pytest.raises(
TypeCheckError, match=r'argument "value" \(str\) is not an instance of int'
):
clamp("5", 0, 10)
def test_return_value_is_checked():
@typechecked
def bad() -> int:
return "nope" # type: ignore[return-value]
with pytest.raises(
TypeCheckError, match=r"the return value \(str\) is not an instance of int"
):
bad()
def test_check_type_returns_value_and_raises():
assert check_type(3, int) == 3
with pytest.raises(TypeCheckError, match="str is not an instance of int"):
check_type("3", int)
def test_container_check_is_first_item_by_default():
# the default strategy only inspects element 0, so a bad tail slips through
assert check_type([1, "a"], list[int]) == [1, "a"]
with pytest.raises(TypeCheckError, match="item 1 of list is not an instance of int"):
check_type(
[1, "a"],
list[int],
collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
)
assert parse_ids([1, 2]) == [1, 2]
with pytest.raises(TypeCheckError, match="item 1 of list is not an instance of int"):
parse_ids([1, "2"])
def test_sequence_accepts_numeric_tower():
assert mean((1, 2, 3)) == 2.0
with pytest.raises(
TypeCheckError,
match=r'item 0 of argument "values" \(str\) is neither float or int',
):
mean("abc")
def test_only_bounded_typevars_are_enforced():
assert checked_sum(1, 2.5) == 3.5
with pytest.raises(
TypeCheckError,
match=r'argument "b" \(str\) is not an instance of numbers.Number',
):
checked_sum(1, "a")
# an unbounded TypeVar is treated as Any: no cross argument unification
assert pair(1, "a") == (1, "a")
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.