Skip to main content

Validate and parse function inputs

Project description

valimp

PyPI Python Support Code style: ruff pre-commit.ci status

In Python use type hints to validate, parse and coerce inputs to public functions and dataclasses.

This is the sole use of valimp. It's a single short module with no depenencies that does one thing and makes it simple to do.

Works like this:

from valimp import parse, Parser, Coerce
from typing import Annotated, Union, Optional, Any

@parse  # add the `valimp.parse`` decorator to a public function or method
def public_function(
    # validate against built-in or custom types
    a: str,
    /,  # support for positional-only arguments
    # support for type unions
    b: Union[int, float],  # or from Python 3.10 `int | float`
    # validate type of container items
    c: dict[str, Union[int, float]],  # dict[str, int | float]
    # coerce input to a specific type
    d: Annotated[
        Union[int, float, str],  # int | float | str
        Coerce(int)
    ],
    # parse input with reference to earlier inputs...
    e: Annotated[
        str,
        Parser(lambda name, obj, params: obj + f"_{name}_{params['a']}")
    ],
    # coerce and parse input...
    f: Annotated[
        Union[str, int],  # str | int
        Coerce(str),
        Parser(lambda name, obj, _: obj + f"_{name}")
    ],
    # validate input is a class (rather than an instance)
    g: type,
    # validate input is subclass of a specific class (or that class itself) ...
    h: type[int],
    # ... or of specific classes...
    i: type[Union[int, str]],  # type[int | str]
    # support for packing extra arguments if required, can be optionally typed...
    *args: Annotated[
        Union[int, float, str],  # int | float | str
        Coerce(int)
    ],
    # support for optional types
    j: Optional[str],  # str | None
    # define default values dynamically with reference to earlier inputs
    k: Annotated[
        Optional[float],  # float | None
        Parser(lambda _, obj, params: params["b"] if obj is None else obj)
    ] = None,
    # support for packing excess kwargs if required, can be optionally typed...
    # **kwargs: Union[int, float]
) -> dict[str, Any]:
    return {"a":a, "b":b, "c":c, "d":d, "e":e, "f":f, "g":g, "h":h, "i":i, "args":args, "j":j, "k":k}

public_function(
    # NB 'a' must be passed positionally, 'b' through 'i' can be passed positionally
    "zero",  # a
    1.0,  # b
    {"two": 2},  # c
    3.3,  # d, will be coerced from float to int, i.e. to 3
    "four",  # e, will be parsed to "four_e_zero"
    5,  # f, will be coerced to str and then parsed to "5_f"
    str,  # g
    bool,  # h, a subclass of int
    int,  # i, one of the subscripted classes
    "10",  # extra arg, will be coerced to int and packed
    20,  # extra arg, will be packed
    j="keyword_arg_j",
    # k, not passed, will be assigned dynamically as parameter b (i.e. 1.0)
)

returns:

{'a': 'zero',
 'b': 1.0,
 'c': {'two': 2},
 'd': 3,
 'e': 'four_e_zero',
 'f': '5_f',
 'g': <class 'str'>,
 'h': <class 'bool'>,
 'i': <class 'int'>,
 'args': (10, 20),
 'j': 'keyword_arg_j',
 'k': 1.0}

And if there are invalid inputs...

public_function(
    ["not a string"],  # INVALID
    b="not an int or a float",  # INVALID
    c={2: "two"},  # INVALID, key not a str and value not an int or float
    d=3.2, # valid input
    e="valid input",
    f=5.0,  # INVALID, not a str or an int
    g=str,  # valid input
    h=str,  # INVALID, str is not int or a subclass of int
    i=bool,  # valid input
    j="valid input",
)

raises:

InputsError: The following inputs to 'public_function' do not conform with the corresponding type annotation:

a
	Takes type <class 'str'> although received '['not a string']' of type <class 'list'>.

b
	Takes input that conforms with <(<class 'int'>, <class 'float'>)> although received 'not an int or a float' of type <class 'str'>.

c
	Takes type <class 'dict'> with keys that conform to the first argument and values that conform to the second argument of <dict[str, typing.Union[int, float]]>, although the received dictionary contains an item with key '2' of type <class 'int'> and value 'two' of type <class 'str'>.

f
	Takes input that conforms with <(<class 'str'>, <class 'int'>)> although received '5.0' of type <class 'float'>.

h
	Takes a subclass of <class 'int'> although received '<class 'str'>'.

And if the inputs do not match the signature...

public_function(
    "zero",
    "invalid input",  # invalid (not int or float), included in errors
    {"two": 2},
    3.2,
    # no argument passed for required positional args 'e', 'f', 'g', 'h' and 'i'
    a="a again",  # 'a' is positional-only: cannot be passed as a kwarg unless sig has **kwargs
    c={"three": 3},  # passing multiple values for 'c'
    not_a_kwarg="not a kwarg",  # including an unexpected kwarg
    # no argument passed for required keyword arg 'j'
)

raises:

InputsError: Inputs to 'public_function' do not conform with the function signature:

Got multiple values for argument: 'c'.

Got unexpected keyword argument: 'not_a_kwarg'.

Got positional-only argument as keyword argument (and signature makes no provision for **kwargs that would otherwise receive it): 'a'.

Missing 5 positional arguments: 'e', 'f', 'g', 'h' and 'i'.

Missing 1 keyword-only argument: 'j'.

The following inputs to 'public_function' do not conform with the corresponding type annotation:

b
	Takes input that conforms with <(<class 'int'>, <class 'float'>)> although received 'invalid input' of type <class 'str'>.

Use all the same functionality to validate, parse and coerce the fields of a dataclass...

from valimp import parse_cls
import dataclasses

@parse_cls  # place valimp decorator above the dataclass decorator
@dataclasses.dataclass
class ADataclass:
    
    a: str
    b: Annotated[
        Union[str, int],
        Coerce(str),
        Parser(lambda name, obj, params: obj + f" {name} {params['a']}")
    ]

rtrn = ADataclass("I'm a and will appear at the end of b", 33)
dataclasses.asdict(rtrn)

output:

{'a': "I'm a and will appear at the end of b",
 'b': "33 b I'm a and will appear at the end of b"}

Installation

$ pip install valimp

No dependencies!

Documentation

tutorial.ipynb offers a walk-through of all the functionality.

Further documentation can be found in the module docstring of valimp.py.

Why another validation library!?

Why even validate input type?

Some may argue that validating the type of public inputs is not pythonic and we can 'duck' out of it and let the errors arise where they may. I'd argue that for the sake of adding a decorator I'd rather raise an intelligible error message than have to respond to an issue asking 'why am I getting this error...'.

:information_source: valimp is only intended for handling inputs to public functions and dataclasses. For internal validation, consider using a type checker (for example, mypy).

Also, I like the option of abstracting away all parsing, coercion and validation of public inputs and just receiving the formal parameter as required. For example, public methods in market-prices often include a 'date' parameter. I like to offer users the convenience to pass this as either a str, a datetime.date or a pandas.Timestamp, although internally I want it as a pandas.Timestamp. I can do this with Valimp by simply including Coerce(pandas.Timestamp) to the metadata of the type annotation of each 'date' parameter. I also need to validate that the input is timezone-naive and does indeed represent a date rather than a time. I can do this by defining a single valimp.Parser and similarly including it to the annotation metadata of the 'date' parameters. Everything's abstracted away. With a little understanding of type annotations the user can see what's going on by simple inspection of the function's signature (as included within the standard help).

Why wouldn't I just use Pydantic?

Pydantic is orientated towards the validation of inputs to dataclasses. Whilst the Valimp @parse_cls decorator does this well for non-complex cases, if you're looking to do more then Pydantic is the place to go.

As for validating public function input, in the early releases of Pydantic V2 the @validate_call decorator failed to provide for validating later parameters based on values received by earlier parameters (a regression from the Pydantic V1 @validate_arguments decorator). This loss of functionality, together with finding Pydantic somewhat clunky to do anything beyond simple type validation, is what led me to write valimp. (I believe functionality to validate later parameters based on values receive by earlier parameters may have since been restored in Pydantic V2, see the issue.)

In short, if you only want to validate the type of function inputs then Pydantic V2 @validate_call will do the trick. If you're after additional validation, parsing or coercion then chances are you'll find valimp to be a simpler option.

What's supported

valimp supports:

  • use of the following type annotations:
    • built-in classes, for example int, str, list, dict etc
    • custom classes
    • collections.abc.Sequence
    • collections.abc.Mapping
    • typing.Any
    • typing.Literal
    • typing.Union ( | from 3.10 )
    • typing.Optional ( <cls> | None from 3.10)
    • collections.abc.Callable, although validation of subscripted types is not supported
    • type, including subscripted types, for example type[int], to validate that an input is a subclass of the subscripted type
  • validation of container items for the following generic classes:
    • list
    • dict
    • tuple
    • set
    • collections.abc.Sequence
    • collections.abc.Mapping
  • packing and optionally coercing, parsing and validating packed objects, i.e. objects received to, for example, *args and **kwargs.
  • full verification of signature compliance in accordance with standard Python, i.e. verifies:
    • no excess positional arguments
    • no unexpected keyword arguments (if the signature does not provide for **kwargs)
    • no missing 'required' arguments (i.e. arguments that do not otherwise have a default value)
    • no duplicate arguments
    • postional-only arguments are passed positionally. (If a keyword argument is passed with the same name as a positional-only argument then it will be considered valid if the signature provides for **kwargs (and in this case it will be received by **kwargs), whilst if the signature does not provide for **kwargs then the argument will be considered invalid.)
    • positional arguments are passed either positionally or by keyword argument

valimp does NOT support:

  • Validation of subscripted types in collections.abc.Callable. Any subscriptions to Callable are ignored (for example a_param: Callable[[int, str], [str]). valimp is concerned with validating the types of arguments passed at runtime, insepction of callables' signatures is outside the scope of the package. (Note that valimp does verify that an object passed to a parameter annotated as Callable is in fact callable).

The library has been built with development in mind and PRs are very much welcome!

License

MIT License

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

valimp-0.6.tar.gz (84.1 kB view details)

Uploaded Source

Built Distribution

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

valimp-0.6-py3-none-any.whl (18.9 kB view details)

Uploaded Python 3

File details

Details for the file valimp-0.6.tar.gz.

File metadata

  • Download URL: valimp-0.6.tar.gz
  • Upload date:
  • Size: 84.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for valimp-0.6.tar.gz
Algorithm Hash digest
SHA256 c4c84fe5b4150f6882569e778fbb5731cfff09f3d47c1a63ccdc9d9d2ceaf56a
MD5 bd6c7ffccf91ed140793b004bc54f596
BLAKE2b-256 84a0b84ccb17554e02dcd838fb89b391045192fab6f09852a476e64ac768d096

See more details on using hashes here.

File details

Details for the file valimp-0.6-py3-none-any.whl.

File metadata

  • Download URL: valimp-0.6-py3-none-any.whl
  • Upload date:
  • Size: 18.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for valimp-0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 2afa617cbaa5628dc9bf6466b8bbb0987f59a2570f40c015d14c56a579d7e2e5
MD5 f70fb74077c784d6f65d3d2c1859f75a
BLAKE2b-256 6ab7d0d9e9c0e7f41052b7b78410e23467ea3bb3f15259e9d255c07875515ccd

See more details on using hashes here.

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