Skip to content

c108.typing

Runtime type validation utilities.

valid_types(func=None, *, skip=None, only=None)

Decorator to validate function arguments against type hints at runtime.

Creates a wrapper around the function that checks argument types before each call. By default, validates all annotated parameters. Use skip or only to control which parameters are checked.

Parameters:

Name Type Description Default
func

The function to wrap (automatically provided when used as @valid_types).

None
skip Iterable[str]

Parameter names to exclude from validation. Cannot be used with only.

None
only Iterable[str]

Parameter names to validate (all others ignored). Cannot be used with skip.

None

Returns:

Type Description

A wrapper function that performs type validation before calling the original function.

Raises:

Type Description
TypeError

If a validated argument doesn't match its type hint at call time.

ValueError

If both skip and only are provided (mutually exclusive), or if invalid parameter names are specified.

Examples:

Validate all parameters:

@valid_types
def connect(host: str, port: int) -> None:
    pass

Validate only specific parameters (gradual adoption):

@valid_types(only=("host",))
def connect(host: str, port: int, options: dict) -> None:
    pass

Skip validation for specific parameters:

@valid_types(skip=("payload",))
def send(id: int, payload: dict) -> None:
    pass
Notes
  • This is a function decorator that wraps the original function
  • Validation happens once per function call with minimal overhead
  • Only validates parameters with type hints; unannotated params are ignored
  • typing.Any hints are never validated
  • Generic types (e.g., List[int]) validate the container type only, not contents
  • Works with both positional and keyword arguments
  • Union and Optional types validate against all union members
  • Supports both Union[X, Y] and X | Y syntax (Python 3.10+)
Source code in c108/typing.py
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def valid_types(func=None, *, skip: typing.Iterable[str] = None, only: typing.Iterable[str] = None):
    """
    Decorator to validate function arguments against type hints at runtime.

    Creates a wrapper around the function that checks argument types before each call.
    By default, validates all annotated parameters. Use `skip` or `only` to control
    which parameters are checked.

    Args:
        func: The function to wrap (automatically provided when used as @valid_types).
        skip: Parameter names to exclude from validation. Cannot be used with `only`.
        only: Parameter names to validate (all others ignored). Cannot be used with `skip`.

    Returns:
        A wrapper function that performs type validation before calling the original function.

    Raises:
        TypeError: If a validated argument doesn't match its type hint at call time.
        ValueError: If both `skip` and `only` are provided (mutually exclusive),
                    or if invalid parameter names are specified.

    Examples:
        Validate all parameters:

            @valid_types
            def connect(host: str, port: int) -> None:
                pass

        Validate only specific parameters (gradual adoption):

            @valid_types(only=("host",))
            def connect(host: str, port: int, options: dict) -> None:
                pass

        Skip validation for specific parameters:

            @valid_types(skip=("payload",))
            def send(id: int, payload: dict) -> None:
                pass

    Notes:
        - This is a function decorator that wraps the original function
        - Validation happens once per function call with minimal overhead
        - Only validates parameters with type hints; unannotated params are ignored
        - ``typing.Any`` hints are never validated
        - Generic types (e.g., ``List[int]``) validate the container type only, not contents
        - Works with both positional and keyword arguments
        - ``Union`` and ``Optional`` types validate against all union members
        - Supports both ``Union[X, Y]`` and ``X | Y`` syntax (Python 3.10+)
    """

    if func is None:
        return functools.partial(valid_types, skip=skip, only=only)

    # 0. VALIDATION: Mutual Exclusivity
    if skip is not None and only is not None:
        raise ValueError(
            f"@valid_types: Cannot use both 'skip' and 'only' on function '{func.__name__}'."
        )

    # 1. SETUP PHASE (runs once at decoration time)
    annotations = typing.get_type_hints(func)
    sig = inspect.signature(func)
    params = sig.parameters

    # Convert iterables to sets for O(1) lookups, handle None
    skip_set = set(skip) if skip else set()
    only_set = set(only) if only is not None else None

    # Validate that requested parameter names actually exist
    param_names = set(params.keys())
    if only_set is not None:
        invalid = only_set - param_names
        if invalid:
            raise ValueError(
                f"@valid_types on '{func.__name__}': 'only' contains invalid parameter names: {invalid}"
            )

    if skip_set:
        invalid = skip_set - param_names
        if invalid:
            raise ValueError(
                f"@valid_types on '{func.__name__}': 'skip' contains invalid parameter names: {invalid}"
            )

    # Build list of (index, name, type) tuples for runtime checking
    arg_checks = []

    for i, (name, param) in enumerate(params.items()):
        # Always skip 'return' annotation
        if name == "return":
            continue

        # LOGIC: Determine if we should check this argument
        should_check = False

        if only_set is not None:
            # Mode: ONLY (allow-list)
            # We only check if it is explicitly in the set
            if name in only_set:
                should_check = True
        else:
            # Mode: SKIP (block-list) or DEFAULT
            # We check if it is NOT in the skip set
            if name not in skip_set:
                should_check = True

        # Final gate: Does it actually have a type hint?
        if should_check and name in annotations:
            hint = annotations[name]

            # Type Normalization: Handle generic types and Union
            origin = typing.get_origin(hint)

            # Check if this is a Union type (typing.Union or types.UnionType from X | Y)
            if origin in UNION_TYPES:
                # Union[str, None] or Optional[str] or str | None -> check against tuple of types
                check_type = typing.get_args(hint)
            elif origin is not None:
                # List[int] -> list, Dict[str, int] -> dict, etc.
                check_type = origin
            else:
                # Plain type like str, int, etc.
                check_type = hint

            # Skip typing.Any (no runtime checking possible)
            if check_type is typing.Any:
                continue

            arg_checks.append((i, name, check_type))

    # 2. RUNTIME WRAPPER (fast path - just a simple loop)
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        for i, name, type_ in arg_checks:
            # Check positional argument
            if i < len(args):
                value = args[i]
                if not isinstance(value, type_):
                    # Format type name(s) nicely for error message
                    if isinstance(type_, tuple):
                        expected = " | ".join(
                            t.__name__ if hasattr(t, "__name__") else str(t) for t in type_
                        )
                    else:
                        expected = type_.__name__ if hasattr(type_, "__name__") else str(type_)

                    raise TypeError(
                        f"argument '{name}' expected {expected}, got {type(value).__name__}"
                    )
            # Check keyword argument
            elif name in kwargs:
                value = kwargs[name]
                if not isinstance(value, type_):
                    # Format type name(s) nicely for error message
                    if isinstance(type_, tuple):
                        expected = " | ".join(
                            t.__name__ if hasattr(t, "__name__") else str(t) for t in type_
                        )
                    else:
                        expected = type_.__name__ if hasattr(type_, "__name__") else str(type_)

                    raise TypeError(
                        f"argument '{name}' expected {expected}, got {type(value).__name__}"
                    )

        return func(*args, **kwargs)

    return wrapper

validate_attr_types(obj, *, attrs=None, include_inherited=True, include_private=False, pattern=None, strict_unions=True, strict_none=True, strict_missing=True)

Validate that object attributes match their type annotations.

Supports dataclasses, attrs classes, and regular Python classes with type annotations. Performance-optimized with automatic fast path for dataclasses.

This function validates the types of object attributes. For validating function parameters, see validate_param_types() (inline) or @valid_param_types (decorator).

Parameters:

Name Type Description Default
obj Any

Object instance to validate

required
attrs list[str] | None

Optional list of specific attribute names to validate. If None, validates all annotated attributes.

None
include_inherited bool

If True, validates inherited attributes with type hints

True
include_private bool

If True, validates private attributes (starting with '_')

False
pattern str | None

Optional regex pattern to filter which attributes to validate

None
strict_unions bool

If True (default), raise TypeError when encountering Union types that cannot be validated with isinstance() (e.g., list[int] | dict[str, int], Callable[[int], str] | Callable[[str], int]). If False, silently skip such unions. Simple unions like int | str | None are always validated regardless of this flag.

True
strict_none bool

If True (default), None values only pass validation when explicitly allowed in the type hint via Optional or Union with None (strict enforcement mode). If False, None values pass validation for ANY type hint (lenient mode, useful for development/migration).

True
strict_missing bool

If True (default), raise TypeError when an attribute has a type annotation but is missing from the object (AttributeError when accessing it). If False, silently skip missing attributes. This is important for production validation to catch incomplete objects.

True

Raises:

Type Description
TypeError

If attribute type doesn't match annotation, or if strict_unions=True and a truly unvalidatable Union type is encountered

ValueError

If obj has no type annotations

RuntimeError

If Python version < 3.11

🚀 Performance: Automatic optimization: - Fast path (~5-10µs): Used for dataclasses when attrs=None, pattern=None, and include_private=False - Slow path (~30-70µs): Used for all other cases (non-dataclasses, pattern matching, custom attr lists, private attrs) - You don't need to configure anything - the function automatically chooses the optimal path based on your parameters

The fast path is 5-10x faster and recommended for high-throughput scenarios
like validation in __post_init__ or API request handlers.
Validation Modes

Strict (default - strict_none=True, strict_missing=True): >>> class Config: ... timeout: int = None # None fails - not in type hint >>> validate_attr_types(Config()) # doctest: +SKIP >>> # ❌ Raises TypeError

>>> class Config:
...     timeout: int | None = None  # None passes - explicitly in hint
>>> validate_attr_types(Config())  # ✅ Passes

>>> class Config:
...     timeout: int  # Annotated but not set
>>> validate_attr_types(Config())  # doctest: +SKIP
>>> # ❌ Raises TypeError (missing attribute)

Lenient (strict_none=False, strict_missing=False): >>> class Config: ... timeout: int = None # None passes despite int hint >>> validate_attr_types(Config(), strict_none=False) # ✅ Passes

>>> class Config:
...     timeout: int  # Missing attribute ignored
>>> validate_attr_types(Config(), strict_missing=False)  # ✅ Passes
See Also

validate_param_types(): Validate function parameter types (inline call) valid_param_types: Decorator for automatic parameter type validation

Examples:

>>> from dataclasses import dataclass
>>>
>>> @dataclass
... class ImageData:
...     id: str = "qwkfjkqfjhkgwdjhg349893874"
...     width: int = 1080
...     height: int = 1080
...
...     def __post_init__(self):
...         validate_attr_types(self)  # Auto-uses fast path
>>>
>>> obj = ImageData()
>>>
>>> # Validate with default settings (strict mode)
>>> validate_attr_types(obj)
>>>
>>> # Lenient None checking
>>> validate_attr_types(obj, strict_none=False)
>>>
>>> # Allow missing attributes
>>> validate_attr_types(obj, strict_missing=False)
>>>
>>> # Use pattern matching (automatically uses slow path)
>>> validate_attr_types(obj, pattern=r"^api_.*")
>>>
>>> # Validate after mutations
>>> obj.width = "invalid"
>>> validate_attr_types(obj)  # Raises TypeError
Traceback (most recent call last):
...
TypeError: type validation failed in <ImageData>:
  Attribute 'width' must be <int>, got <str>
>>> # For function parameters, use validate_param_types() or @valid_param_types
>>> def process(x: int, y: str):
...     validate_param_types()  # Inline validation
...     # ... or use @valid_param_types decorator
Source code in c108/typing.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
def validate_attr_types(
    obj: Any,
    *,
    attrs: list[str] | None = None,
    include_inherited: bool = True,
    include_private: bool = False,
    pattern: str | None = None,
    strict_unions: bool = True,
    strict_none: bool = True,
    strict_missing: bool = True,
) -> None:
    """
    Validate that object attributes match their type annotations.

    Supports dataclasses, attrs classes, and regular Python classes with
    type annotations. Performance-optimized with automatic fast path for dataclasses.

    This function validates the types of object attributes. For validating
    function parameters, see validate_param_types() (inline) or @valid_param_types
    (decorator).

    Args:
        obj: Object instance to validate
        attrs: Optional list of specific attribute names to validate.
               If None, validates all annotated attributes.
        include_inherited: If True, validates inherited attributes with type hints
        include_private: If True, validates private attributes (starting with '_')
        pattern: Optional regex pattern to filter which attributes to validate
        strict_unions: If True (default), raise TypeError when encountering Union types
                       that cannot be validated with isinstance() (e.g., list[int] | dict[str, int],
                       Callable[[int], str] | Callable[[str], int]). If False, silently skip
                       such unions. Simple unions like int | str | None are always validated
                       regardless of this flag.
        strict_none: If True (default), None values only pass validation when explicitly
                     allowed in the type hint via Optional or Union with None (strict
                     enforcement mode). If False, None values pass validation for ANY type
                     hint (lenient mode, useful for development/migration).
        strict_missing: If True (default), raise TypeError when an attribute has a type
                        annotation but is missing from the object (AttributeError when
                        accessing it). If False, silently skip missing attributes. This is
                        important for production validation to catch incomplete objects.

    Raises:
        TypeError: If attribute type doesn't match annotation, or if strict_unions=True
                   and a truly unvalidatable Union type is encountered
        ValueError: If obj has no type annotations
        RuntimeError: If Python version < 3.11

    🚀 Performance:
        Automatic optimization:
            - Fast path (~5-10µs): Used for dataclasses when attrs=None, pattern=None,
              and include_private=False
            - Slow path (~30-70µs): Used for all other cases (non-dataclasses, pattern
              matching, custom attr lists, private attrs)
            - You don't need to configure anything - the function automatically chooses
              the optimal path based on your parameters

        The fast path is 5-10x faster and recommended for high-throughput scenarios
        like validation in __post_init__ or API request handlers.

    Validation Modes:
        Strict (default - strict_none=True, strict_missing=True):
            >>> class Config:
            ...     timeout: int = None  # None fails - not in type hint
            >>> validate_attr_types(Config())  # doctest: +SKIP
            >>> # ❌ Raises TypeError

            >>> class Config:
            ...     timeout: int | None = None  # None passes - explicitly in hint
            >>> validate_attr_types(Config())  # ✅ Passes

            >>> class Config:
            ...     timeout: int  # Annotated but not set
            >>> validate_attr_types(Config())  # doctest: +SKIP
            >>> # ❌ Raises TypeError (missing attribute)

        Lenient (strict_none=False, strict_missing=False):
            >>> class Config:
            ...     timeout: int = None  # None passes despite int hint
            >>> validate_attr_types(Config(), strict_none=False)  # ✅ Passes

            >>> class Config:
            ...     timeout: int  # Missing attribute ignored
            >>> validate_attr_types(Config(), strict_missing=False)  # ✅ Passes

    See Also:
        validate_param_types(): Validate function parameter types (inline call)
        valid_param_types: Decorator for automatic parameter type validation

    Examples:
        >>> from dataclasses import dataclass
        >>>
        >>> @dataclass
        ... class ImageData:
        ...     id: str = "qwkfjkqfjhkgwdjhg349893874"
        ...     width: int = 1080
        ...     height: int = 1080
        ...
        ...     def __post_init__(self):
        ...         validate_attr_types(self)  # Auto-uses fast path
        >>>
        >>> obj = ImageData()
        >>>
        >>> # Validate with default settings (strict mode)
        >>> validate_attr_types(obj)
        >>>
        >>> # Lenient None checking
        >>> validate_attr_types(obj, strict_none=False)
        >>>
        >>> # Allow missing attributes
        >>> validate_attr_types(obj, strict_missing=False)
        >>>
        >>> # Use pattern matching (automatically uses slow path)
        >>> validate_attr_types(obj, pattern=r"^api_.*")
        >>>
        >>> # Validate after mutations
        >>> obj.width = "invalid"
        >>> validate_attr_types(obj)  # Raises TypeError
        Traceback (most recent call last):
        ...
        TypeError: type validation failed in <ImageData>:
          Attribute 'width' must be <int>, got <str>

        >>> # For function parameters, use validate_param_types() or @valid_param_types
        >>> def process(x: int, y: str):
        ...     validate_param_types()  # Inline validation
        ...     # ... or use @valid_param_types decorator
    """
    # Determine if we can use fast path for a dataclass
    is_dc = is_dataclass(obj)
    can_use_fast = is_dc and attrs is None and pattern is None and not include_private

    # Automatically choose the optimal path
    if can_use_fast:
        _validate_attr_dataclass_fast(
            obj,
            strict_unions=strict_unions,
            strict_none=strict_none,
            strict_missing=strict_missing,
        )
    else:
        _validate_attr_with_search(
            obj,
            attrs=attrs,
            include_inherited=include_inherited,
            include_private=include_private,
            pattern=pattern,
            strict_unions=strict_unions,
            strict_none=strict_none,
            strict_missing=strict_missing,
        )

validate_param_types(*, skip=None, only=None)

Validate function parameters against their type hints (inline validation).

Must be called from within a function to inspect its parameters and annotations. Uses the calling frame to automatically detect the function and its arguments.

This is the inline validation approach. For automatic validation via decorator, use @valid_types instead.

Parameters:

Name Type Description Default
skip Iterable[str] | None

Parameter names to exclude from validation. Cannot be used with only.

None
only Iterable[str] | None

Parameter names to validate (all others ignored). Cannot be used with skip.

None

Raises:

Type Description
TypeError

If a validated parameter doesn't match its type hint

ValueError

If both skip and only are provided (mutually exclusive), or if invalid parameter names are specified

RuntimeError

If called outside a function context or function cannot be found

Union Type Support

Supported (always validated): - Simple unions: int | str | float - Optional types: str | None, int | None - Union of basic types: list | dict | tuple

Unsupported (silently skipped): - Parameterized generic unions: list[int] | dict[str, int] - Callable unions with different signatures: Callable[[int], str] | Callable[[str], int]

🚀 Performance: ~50-100µs first call, ~10-20µs subsequent calls For hot paths, consider using @valid_types decorator instead (~5-15µs)

See Also

valid_types: Decorator for automatic parameter type validation (faster) validate_attr_types(): Validate object attribute types

Examples:

>>> # Basic usage
>>> def process_data(user_id: int, name: str | None, score: float = 0.0):
...     validate_param_types()
...     return f"{user_id}: {name} ({score})"
...
>>> process_data(101, "Alice", 98.5)
'101: Alice (98.5)'
>>> process_data("invalid", "Alice", 98.5)  # ❌ Raises TypeError
Traceback (most recent call last):
...
TypeError: type validation failed in process_data():
  Parameter 'user_id' must be <int>, got <str>
>>> # Validate only specific parameters
>>> def api_endpoint(user_id: int, token: str, debug: bool = False):
...     validate_param_types(only=["user_id", "token"])  # Skip 'debug'
...     # ... rest of function
...
>>> # Skip certain parameters
>>> def send_message(id: int, payload: dict, metadata: dict):
...     validate_param_types(skip=["metadata"])  # Skip metadata validation
...     # ... rest of function
...
>>> # Works with instance methods (automatically skips 'self')
>>> class DataProcessor:
...     def process(self, data: int | str, strict_mode: bool = False):
...         validate_param_types()  # Skips 'self' automatically
...         # ... rest of method
...
>>> processor = DataProcessor()
>>> processor.process(42)  # ✅ Passes
>>> processor.process(3.14)  # ❌ Raises TypeError
Traceback (most recent call last):
...
TypeError: type validation failed in process():
  Parameter 'data' must be <int> | <str>, got <float>
>>> # Conditional validation (advantage over decorator)
>>> def handle_request(data: dict, mode: str):
...     if mode == "strict":
...         validate_param_types()
...     # ... rest of function
...
>>> # For standard cases, decorator is cleaner:
>>> @valid_types
... def process(data: int | str):
...     return f"Processed {data}"
...
>>> process(42)
'Processed 42'
Note

Use @valid_types decorator when possible for better performance (~3x faster). Use this inline version when: - You want validation hidden from function signature (cleaner public API) - You need conditional validation based on runtime logic - You're retrofitting validation into existing code without changing signatures

Source code in c108/typing.py
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
def validate_param_types(
    *,
    skip: typing.Iterable[str] | None = None,
    only: typing.Iterable[str] | None = None,
) -> None:
    """
    Validate function parameters against their type hints (inline validation).

    Must be called from within a function to inspect its parameters and annotations.
    Uses the calling frame to automatically detect the function and its arguments.

    This is the inline validation approach. For automatic validation via decorator,
    use @valid_types instead.

    Args:
        skip: Parameter names to exclude from validation. Cannot be used with `only`.
        only: Parameter names to validate (all others ignored). Cannot be used with `skip`.

    Raises:
        TypeError: If a validated parameter doesn't match its type hint
        ValueError: If both `skip` and `only` are provided (mutually exclusive),
                    or if invalid parameter names are specified
        RuntimeError: If called outside a function context or function cannot be found

    Union Type Support:
        **Supported (always validated):**
            - Simple unions: int | str | float
            - Optional types: str | None, int | None
            - Union of basic types: list | dict | tuple

        **Unsupported (silently skipped):**
            - Parameterized generic unions: list[int] | dict[str, int]
            - Callable unions with different signatures: Callable[[int], str] | Callable[[str], int]

    🚀 Performance:
        ~50-100µs first call, ~10-20µs subsequent calls
        For hot paths, consider using @valid_types decorator instead (~5-15µs)

    See Also:
        valid_types: Decorator for automatic parameter type validation (faster)
        validate_attr_types(): Validate object attribute types

    Examples:
        >>> # Basic usage
        >>> def process_data(user_id: int, name: str | None, score: float = 0.0):
        ...     validate_param_types()
        ...     return f"{user_id}: {name} ({score})"
        ...
        >>> process_data(101, "Alice", 98.5)
        '101: Alice (98.5)'

        >>> process_data("invalid", "Alice", 98.5)  # ❌ Raises TypeError
        Traceback (most recent call last):
        ...
        TypeError: type validation failed in process_data():
          Parameter 'user_id' must be <int>, got <str>

        >>> # Validate only specific parameters
        >>> def api_endpoint(user_id: int, token: str, debug: bool = False):
        ...     validate_param_types(only=["user_id", "token"])  # Skip 'debug'
        ...     # ... rest of function
        ...
        >>> # Skip certain parameters
        >>> def send_message(id: int, payload: dict, metadata: dict):
        ...     validate_param_types(skip=["metadata"])  # Skip metadata validation
        ...     # ... rest of function
        ...
        >>> # Works with instance methods (automatically skips 'self')
        >>> class DataProcessor:
        ...     def process(self, data: int | str, strict_mode: bool = False):
        ...         validate_param_types()  # Skips 'self' automatically
        ...         # ... rest of method
        ...
        >>> processor = DataProcessor()
        >>> processor.process(42)  # ✅ Passes
        >>> processor.process(3.14)  # ❌ Raises TypeError
        Traceback (most recent call last):
        ...
        TypeError: type validation failed in process():
          Parameter 'data' must be <int> | <str>, got <float>

        >>> # Conditional validation (advantage over decorator)
        >>> def handle_request(data: dict, mode: str):
        ...     if mode == "strict":
        ...         validate_param_types()
        ...     # ... rest of function
        ...
        >>> # For standard cases, decorator is cleaner:
        >>> @valid_types
        ... def process(data: int | str):
        ...     return f"Processed {data}"
        ...
        >>> process(42)
        'Processed 42'

    Note:
        Use @valid_types decorator when possible for better performance (~3x faster).
        Use this inline version when:
        - You want validation hidden from function signature (cleaner public API)
        - You need conditional validation based on runtime logic
        - You're retrofitting validation into existing code without changing signatures
    """
    # 0. VALIDATION: Mutual Exclusivity
    if skip is not None and only is not None:
        raise ValueError("validate_param_types: Cannot use both 'skip' and 'only' parameters.")

    # 1. GET CALLING CONTEXT
    frame = inspect.currentframe()
    if frame is None:
        raise RuntimeError("Cannot get current frame")

    caller_frame = frame.f_back
    if caller_frame is None:
        raise RuntimeError("validate_param_types() must be called from within a function")

    try:
        func_name = caller_frame.f_code.co_name
        local_vars = caller_frame.f_locals.copy()

        # 2. FIND THE FUNCTION OBJECT
        func = _validate_param_get_fn_from_frame(caller_frame, func_name, local_vars)

        if func is None or not callable(func):
            raise RuntimeError(
                f"Cannot find function '{func_name}' to inspect its signature. "
                f"validate_param_types() may not work with:\n"
                f"  - Lambdas (use @valid_types decorator instead)\n"
                f"  - Functions created via exec() or eval()\n"
                f"  - Dynamically generated functions\n"
                f"For these cases, use the @valid_types decorator for automatic validation."
            )

        # 3. GET TYPE HINTS AND SIGNATURE
        try:
            type_hints = get_type_hints(func)
        except Exception:
            # Fallback to annotations if get_type_hints fails
            type_hints = getattr(func, "__annotations__", {}).copy()

        if not type_hints:
            # No type hints - nothing to validate
            return

        sig = inspect.signature(func)
        param_names = set(sig.parameters.keys())

        # 4. CONVERT AND VALIDATE skip/only PARAMETERS
        skip_set = set(skip) if skip else set()
        only_set = set(only) if only is not None else None

        # Validate that requested parameter names actually exist
        if only_set is not None:
            invalid = only_set - param_names
            if invalid:
                raise ValueError(
                    f"validate_param_types in '{func_name}': 'only' contains invalid parameter names: {invalid}"
                )

        if skip_set:
            invalid = skip_set - param_names
            if invalid:
                raise ValueError(
                    f"validate_param_types in '{func_name}': 'skip' contains invalid parameter names: {invalid}"
                )

        # 5. DETERMINE WHICH PARAMETERS TO VALIDATE
        params_to_validate = []

        for param_name in sig.parameters.keys():
            # Skip if no type hint
            if param_name not in type_hints:
                continue

            # Skip 'self' and 'cls' automatically
            if param_name in ("self", "cls"):
                continue

            # Skip if parameter wasn't passed (not in local_vars)
            if param_name not in local_vars:
                continue

            # Apply skip/only logic
            should_validate = False

            if only_set is not None:
                # Mode: ONLY (allow-list)
                should_validate = param_name in only_set
            else:
                # Mode: SKIP (block-list) or DEFAULT
                should_validate = param_name not in skip_set

            if should_validate:
                params_to_validate.append(param_name)

        # 6. VALIDATE EACH PARAMETER
        validation_errors = []

        for param_name in params_to_validate:
            value = local_vars[param_name]
            expected_type = type_hints[param_name]

            # Use shared validation logic
            error = _validate_param_single_value(
                name=param_name,
                name_prefix="parameter",
                value=value,
                expected_type=expected_type,
            )

            if error:
                validation_errors.append(error)

        if validation_errors:
            raise TypeError(
                f"type validation failed in {func_name}():\n  " + "\n  ".join(validation_errors)
            )

    finally:
        # Clean up frame references to avoid reference cycles
        del frame
        del caller_frame