{"id":16811256,"url":"https://github.com/kszucs/koerce","last_synced_at":"2026-03-16T03:35:08.544Z","repository":{"id":250938002,"uuid":"831859540","full_name":"kszucs/koerce","owner":"kszucs","description":"Reusable Pattern Matching on Python Objects","archived":false,"fork":false,"pushed_at":"2024-10-09T07:06:28.000Z","size":226,"stargazers_count":15,"open_issues_count":3,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-18T07:12:16.840Z","etag":null,"topics":["dataclasses","pattern-matching","python","validation"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kszucs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-21T20:22:46.000Z","updated_at":"2025-02-23T20:31:50.000Z","dependencies_parsed_at":"2024-10-28T18:03:18.250Z","dependency_job_id":null,"html_url":"https://github.com/kszucs/koerce","commit_stats":null,"previous_names":["kszucs/koerce"],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kszucs%2Fkoerce","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kszucs%2Fkoerce/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kszucs%2Fkoerce/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kszucs%2Fkoerce/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kszucs","download_url":"https://codeload.github.com/kszucs/koerce/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244902929,"owners_count":20529114,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dataclasses","pattern-matching","python","validation"],"created_at":"2024-10-13T10:18:10.242Z","updated_at":"2026-03-16T03:35:07.966Z","avatar_url":"https://github.com/kszucs.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Performant Python Pattern Matching and Object Validation\n\nReusable pattern matching for Python, implemented in Cython.\nI originally developed this system for the Ibis Project but\nhopefully it can be useful for others as well.\n\nThe implementation aims to be as quick as possible, the pure\npython implementation is already quite fast but taking advantage\nof Cython allows to mitigate the overhead of the Python\ninterpreter.\nI have also tried to use PyO3 but it had higher overhead than\nCython. The current implementation uses the pure python mode\nof cython allowing quick iteration and testing, and then it\ncan be cythonized and compiled to an extension module giving\na significant speedup. Benchmarks shows more than 2x speedup\nover pydantic's model validation which is written in Rust.\n\n## Installation\n\nThe package is published to PyPI, so it can be installed using\npip:\n\n```sh\npip install koerce\n```\n\n## Library components\n\nThe library contains three main components which can be used\nindependently or together:\n\n### 1. Deferred object builders\n\nThese allow delayed evaluation of python expressions given a\ncontext:\n\n```pycon\nIn [1]: from koerce import var, resolve\n\nIn [2]: a, b = var(\"a\"), var(\"b\")\n\nIn [3]: expr = (a + 1) * b[\"field\"]\n\nIn [4]: expr\nOut[4]: (($a + 1) * $b['field'])\n\nIn [5]: resolve(expr, {\"a\": 2, \"b\": {\"field\": 3}})\nOut[5]: 9\n```\n\nThe syntax sugar provided by the deferred objects allows the\ndefinition of complex object transformations in a concise and\nnatural way.\n\n\n### 2. Pattern matchers which operate on various Python objects\n\nPatterns are the heart of the library, they allow **searching**\nand **replacing** specific structures in Python objects. The\nlibrary provides an extensible yet simple way to define patterns\nand match values against them.\n\n```pycon\nIn [1]: from koerce import match, NoMatch, Anything\n\nIn [2]: context = {}\n\nIn [3]: match([1, 2, 3, int, \"a\" @ Anything()], [1, 2, 3, 4, 5], context)\nOut[3]: [1, 2, 3, 4, 5]\n\nIn [4]: context\nOut[4]: {'a': 5}\n```\n\nNote that `from koerce import koerce` function can be used instead\nof `match()` to avoid confusion with the built-in python `match`.\n\n```py\nfrom dataclasses import dataclass\nfrom koerce import Object, match\n\n@dataclass\nclass B:\n    x: int\n    y: int\n    z: float\n\nmatch(Object(B, y=1, z=2), B(1, 1, 2))\n# B(x=1, y=1, z=2)\n```\n\nwhere the `Object` pattern checks whether the passed object is\nan instance of `B` and `value.y == 1` and `value.z == 2` ignoring\nthe `x` field.\n\nPatterns are also able to capture values as variables making the\nmatching process more flexible:\n\n```py\nfrom koerce import var\n\nx = var(\"x\")\n\n# `+x` means to capture that object argument as variable `x`\n# then the `z` argument must match that captured value\nmatch(Object(B, +x, z=x), B(1, 2, 1))\n# it is a match because x and z are equal: B(x=1, y=2, z=1)\n\nmatch(Object(B, +x, z=x), B(1, 2, 0))\n# is is a NoMatch because x and z are unequal\n```\n\nPatterns also suitable for match and replace tasks because they\ncan produce new values:\n\n```py\n# \u003e\u003e operator constructs a `Replace` pattern where the right\n# hand side is a deferred object\nmatch(Object(B, +x, z=x) \u003e\u003e (x, x + 1), B(1, 2, 1))\n# result: (1, 2)\n```\n\nPatterns are also composable and can be freely combined using\noverloaded operators:\n\n```pycon\nIn [1]: from koerce import match, Is, Eq, NoMatch\n\nIn [2]: pattern = Is(int) | Is(str)\n   ...: assert match(pattern, 1) == 1\n   ...: assert match(pattern, \"1\") == \"1\"\n   ...: assert match(pattern, 3.14) is NoMatch\n\nIn [3]: pattern = Is(int) | Eq(1)\n   ...: assert match(pattern, 1) == 1\n   ...: assert match(pattern, None) is NoMatch\n```\n\nPatterns can also be constructed from python typehints:\n\n```pycon\nIn [1]: from koerce import match\n\nIn [2]: class Ordinary:\n   ...:     def __init__(self, x, y):\n   ...:         self.x = x\n   ...:         self.y = y\n   ...:\n   ...:\n   ...: class Coercible(Ordinary):\n   ...:\n   ...:     @classmethod\n   ...:     def __coerce__(cls, value):\n   ...:         if isinstance(value, tuple):\n   ...:             return Coercible(value[0], value[1])\n   ...:         else:\n   ...:             raise ValueError(\"Cannot coerce value to Coercible\")\n   ...:\n\nIn [3]: match(Ordinary, Ordinary(1, 2))\nOut[3]: \u003c__main__.Ordinary at 0x105194fe0\u003e\n\nIn [4]: match(Ordinary, (1, 2))\nOut[4]: koerce.patterns.NoMatch\n\nIn [5]: match(Coercible, (1, 2))\nOut[5]: \u003c__main__.Coercible at 0x109ebb320\u003e\n```\n\nThe pattern creation logic also handles generic types by doing\nlightweight type parameter inference. The implementation is quite\ncompact, available under `Pattern.from_typehint()`.\n\n### 3. A high-level validation system for dataclass-like objects\n\nThis abstraction is similar to what attrs or pydantic provide but\nthere are some differences (TODO listing them).\n\n```pycon\nIn [1]: from typing import Optional\n   ...: from koerce import Annotable\n   ...:\n   ...:\n   ...: class MyClass(Annotable):\n   ...:     x: int\n   ...:     y: float\n   ...:     z: Optional[list[str]] = None\n   ...:\n\nIn [2]: MyClass(1, 2.0, [\"a\", \"b\"])\nOut[2]: MyClass(x=1, y=2.0, z=['a', 'b'])\n\nIn [3]: MyClass(1, 2, [\"a\", \"b\"])\nOut[3]: MyClass(x=1, y=2.0, z=['a', 'b'])\n\nIn [4]: MyClass(\"invalid\", 2, [\"a\", \"b\"])\nOut[4]: # raises validation error\n```\n\nAnnotable object are mutable by default, but can be made immutable\nby passing `immutable=True` to the `Annotable` base class. Often\nit is useful to make immutable objects hashable as well, which can\nbe done by passing `hashable=True` to the `Annotable` base class,\nin this case the hash is precomputed during initialization and\nstored in the object making the dictionary lookups cheap.\n\n```pycon\nIn [1]: from typing import Optional\n   ...: from koerce import Annotable\n   ...:\n   ...:\n   ...: class MyClass(Annotable, immutable=True, hashable=True):\n   ...:     x: int\n   ...:     y: float\n   ...:     z: Optional[tuple[str, ...]] = None\n   ...:\n\nIn [2]: a = MyClass(1, 2.0, [\"a\", \"b\"])\n\nIn [3]: a\nOut[3]: MyClass(x=1, y=2.0, z=('a', 'b'))\n\nIn [4]: a.x = 2\nAttributeError: Attribute 'x' cannot be assigned to immutable instance of type \u003cclass '__main__.MyClass'\u003e\n\nIn [5]: {a: 1}\nOut[5]: {MyClass(x=1, y=2.0, z=('a', 'b')): 1}\n```\n\n## Available Pattern matchers\n\nIt is an incompletee list of the matchers, for more details and\nexamples see `koerce/patterns.py` and `koerce/tests/test_patterns.py`.\n\n### `Anything` and `Nothing`\n\n```pycon\nIn [1]: from koerce import match, Anything, Nothing\n\nIn [2]: match(Anything(), \"a\")\nOut[2]: 'a'\n\nIn [3]: match(Anything(), 1)\nOut[3]: 1\n\nIn [4]: match(Nothing(), 1)\nOut[4]: koerce._internal.NoMatch\n```\n\n### `Eq` for equality matching\n\n```pycon\nIn [1]: from koerce import Eq, match, var\n\nIn [2]: x = var(\"x\")\n\nIn [3]: match(Eq(1), 1)\nOut[3]: 1\n\nIn [4]: match(Eq(1), 2)\nOut[4]: koerce._internal.NoMatch\n\nIn [5]: match(Eq(x), 2, context={\"x\": 2})\nOut[5]: 2\n\nIn [6]: match(Eq(x), 2, context={\"x\": 3})\nOut[6]: koerce._internal.NoMatch\n```\n\n### `Is` for instance matching\n\nCouple simple cases are below:\n\n```pycon\nIn [1]: from koerce import match, Is\n\nIn [2]: class A: pass\n\nIn [3]: match(Is(A), A())\nOut[3]: \u003c__main__.A at 0x1061070e0\u003e\n\nIn [4]: match(Is(A), \"A\")\nOut[4]: koerce._internal.NoMatch\n\nIn [5]: match(Is(int), 1)\nOut[5]: 1\n\nIn [6]: match(Is(int), 3.14)\nOut[6]: koerce._internal.NoMatch\n\nIn [7]: from typing import Optional\n\nIn [8]: match(Is(Optional[int]), 1)\nOut[8]: 1\n\nIn [9]: match(Is(Optional[int]), None)\n```\n\nGeneric types are also supported by checking types of attributes / properties:\n\n```py\nfrom koerce import match, Is, NoMatch\nfrom typing import Generic, TypeVar, Any\nfrom dataclasses import dataclass\n\n\nT = TypeVar(\"T\", covariant=True)\nS = TypeVar(\"S\", covariant=True)\n\n@dataclass\nclass My(Generic[T, S]):\n    a: T\n    b: S\n    c: str\n\n\nMyAlias = My[T, str]\n\nb_int = My(1, 2, \"3\")\nb_float = My(1, 2.0, \"3\")\nb_str = My(\"1\", \"2\", \"3\")\n\n# b_int.a must be an instance of int\n# b_int.b must be an instance of Any\nassert match(My[int, Any], b_int) is b_int\n\n# both b_int.a and b_int.b must be an instance of int\nassert match(My[int, int], b_int) is b_int\n\n# b_int.b should be an instance of a float but it isn't\nassert match(My[int, float], b_int) is NoMatch\n\n# now b_float.b is actually a float so it is a match\nassert match(My[int, float], b_float) is b_float\n\n# type aliases are also supported\nassert match(MyAlias[str], b_str) is b_str\n```\n\n### `As` patterns attempting to coerce the value as the given type\n\n```py\nfrom koerce import match, As, NoMatch\nfrom typing import Generic, TypeVar, Any\nfrom dataclasses import dataclass\n\nclass MyClass:\n    pass\n\nclass MyInt(int):\n    @classmethod\n    def __coerce__(cls, other):\n        return MyInt(int(other))\n\n\nclass MyNumber(Generic[T]):\n    value: T\n\n    def __init__(self, value):\n        self.value = value\n\n    @classmethod\n    def __coerce__(cls, other, T):\n        return cls(T(other))\n\n\nassert match(As(int), 1.0) == 1\nassert match(As(str), 1.0) == \"1.0\"\nassert match(As(float), 1.0) == 1.0\nassert match(As(MyClass), \"myclass\") is NoMatch\n\n# by implementing the coercible protocol objects can be transparently\n# coerced to the given type\nassert match(As(MyInt), 3.14) == MyInt(3)\n\n# coercible protocol also supports generic types where the `__coerce__`\n# method should be implemented on one of the base classes and the\n# type parameters are passed as keyword arguments to `cls.__coerce__()`\nassert match(As(MyNumber[float]), 8).value == 8.0\n```\n\n`As` and `Is` can be omitted because `match()` tries to convert its\nfirst argument to a pattern using the `koerce.pattern()` function:\n\n```py\nfrom koerce import pattern, As, Is\n\nassert pattern(int, allow_coercion=False) == Is(int)\nassert pattern(int, allow_coercion=True) == As(int)\n\nassert match(int, 1, allow_coercion=False) == 1\nassert match(int, 1.1, allow_coercion=False) is NoMatch\n# lossy coercion is not allowed\nassert match(int, 1.1, allow_coercion=True) is NoMatch\n\n# default is allow_coercion=False\nassert match(int, 1.1) is NoMatch\n```\n\n`As[typehint]` and `Is[typehint]` can be used to create patterns:\n\n```py\nfrom koerce import Pattern, As, Is\n\nassert match(As[int], '1') == 1\nassert match(Is[int], 1) == 1\nassert match(Is[int], '1') is NoMatch\n```\n\n### `If` patterns for conditionals\n\nAllows conditional matching based on the value of the object,\nor other variables in the context:\n\n```py\nfrom koerce import match, If, Is, var, NoMatch, Capture\n\nx = var(\"x\")\n\npattern = Capture(x) \u0026 If(x \u003e 0)\nassert match(pattern, 1) == 1\nassert match(pattern, -1) is NoMatch\n```\n\n### `Custom` for user defined matching logic\n\nA function passed to either `match()` or `pattern()` is treated\nas a `Custom` pattern:\n\n```py\nfrom koerce import match, Custom, NoMatch, NoMatchError\n\ndef is_even(value):\n    if value % 2:\n        raise NoMatchError(\"Value is not even\")\n    else:\n        return value\n\nassert match(is_even, 2) == 2\nassert match(is_even, 3) is NoMatch\n```\n\n### `Capture` to record values in the context\n\nA capture pattern can be defined several ways:\n\n```py\nfrom koerce import Capture, Is, var\n\nx = var(\"x\")\n\nCapture(\"x\")  # captures anything as \"x\" in the context\nCapture(x)  # same as above but using a variable\nCapture(\"x\", Is(int))  # captures only integers as \"x\" in the context\nCapture(\"x\", Is(int) | Is(float))  # captures integers and floats as \"x\" in the context\n\"x\" @ Is(int)  # syntax sugar for Capture(\"x\", Is(int))\n+x  # syntax sugar for Capture(x, Anything())\n```\n\n```py\nfrom koerce import match, Capture, var\n\n# context is a mutable dictionary passed along the matching process\ncontext = {}\nassert match(\"x\" @ Is(int), 1, context) == 1\nassert context[\"x\"] == 1\n```\n\n### `Replace` for replacing matched values\n\nAllows replacing matched values with new ones:\n\n```py\nfrom koerce import match, Replace, var\n\nx = var(\"x\")\n\npattern = Replace(Capture(x), x + 1)\nassert match(pattern, 1) == 2\nassert match(pattern, 2) == 3\n```\n\nthere is a syntax sugar for `Replace` patterns, the example above\ncan be written as:\n\n```py\nfrom koerce import match, Replace, var\n\nx = var(\"x\")\n\nassert match(+x \u003e\u003e x + 1, 1) == 2\nassert match(+x \u003e\u003e x + 1, 2) == 3\n```\n\nreplace patterns are especially useful when matching objects:\n\n```py\nfrom dataclasses import dataclass\nfrom koerce import match, Replace, var, namespace\n\nx = var(\"x\")\n\n@dataclass\nclass A:\n    x: int\n    y: int\n\n@dataclass\nclass B:\n    x: int\n    y: int\n    z: float\n\n\np, d = namespace(__name__)\nx, y = var(\"x\"), var(\"y\")\n\n# if value is an instance of A then capture A.0 as x and A.1 as y\n# then construct a new B object with arguments x=x, y=1, z=y\npattern = p.A(+x, +y) \u003e\u003e d.B(x=x, y=1, z=y)\nvalue = A(1, 2)\nexpected = B(x=1, y=1, z=2)\nassert match(pattern, value) == expected\n```\n\nreplacemenets can also be used in nested structures:\n\n```py\nfrom koerce import match, Replace, var, namespace, NoMatch\n\n@dataclass\nclass Foo:\n    value: str\n\n@dataclass\nclass Bar:\n    foo: Foo\n    value: int\n\np, d = namespace(__name__)\n\npattern = p.Bar(p.Foo(\"a\") \u003e\u003e d.Foo(\"b\"))\nvalue = Bar(Foo(\"a\"), 123)\nexpected = Bar(Foo(\"b\"), 123)\n\nassert match(pattern, value) == expected\nassert match(pattern, Bar(Foo(\"c\"), 123)) is NoMatch\n```\n\n### `SequenceOf` / `ListOf` / `TupleOf`\n\n```py\nfrom koerce import Is, NoMatch, match, ListOf, TupleOf\n\npattern = ListOf(str)\nassert match(pattern, [\"foo\", \"bar\"]) == [\"foo\", \"bar\"]\nassert match(pattern, [1, 2]) is NoMatch\nassert match(pattern, 1) is NoMatch\n```\n\n### `MappingOf` / `DictOf` / `FrozenDictOf`\n\n```py\nfrom koerce import DictOf, Is, match\n\npattern = DictOf(Is(str), Is(int))\nassert match(pattern, {\"a\": 1, \"b\": 2}) == {\"a\": 1, \"b\": 2}\nassert match(pattern, {\"a\": 1, \"b\": \"2\"}) is NoMatch\n```\n\n### `PatternList`\n\n```py\nfrom koerce import match, NoMatch, SomeOf, ListOf, pattern\n\nfour = [1, 2, 3, 4]\nthree = [1, 2, 3]\n\nassert match([1, 2, 3, SomeOf(int, at_least=1)], four) == four\nassert match([1, 2, 3, SomeOf(int, at_least=1)], three) is NoMatch\n\ninteger = pattern(int, allow_coercion=False)\nfloating = pattern(float, allow_coercion=False)\n\nassert match([1, 2, *floating], [1, 2, 3]) is NoMatch\nassert match([1, 2, *floating], [1, 2, 3.0]) == [1, 2, 3.0]\nassert match([1, 2, *floating], [1, 2, 3.0, 4.0]) == [1, 2, 3.0, 4.0]\n```\n\n### `PatternMap`\n\n```py\nfrom koerce import match, NoMatch, Is, As\n\npattern = {\n    \"a\": Is(int),\n    \"b\": As(int),\n    \"c\": Is(str),\n    \"d\": ListOf(As(int)),\n}\nvalue = {\n    \"a\": 1,\n    \"b\": 2.0,\n    \"c\": \"three\",\n    \"d\": (4.0, 5.0, 6.0),\n}\nassert match(pattern, value) == {\n    \"a\": 1,\n    \"b\": 2,\n    \"c\": \"three\",\n    \"d\": [4, 5, 6],\n}\nassert match(pattern, {\"a\": 1, \"b\": 2, \"c\": \"three\"}) is NoMatch\n```\n\n## Annotable objects\n\nAnnotable objects are similar to dataclasses but with some differences:\n- Annotable objects are mutable by default, but can be made immutable\n  by passing `immutable=True` to the `Annotable` base class.\n- Annotable objects can be made hashable by passing `hashable=True` to\n  the `Annotable` base class, in this case the hash is precomputed during\n  initialization and stored in the object making the dictionary lookups\n  cheap.\n- Validation strictness can be controlled by passing `allow_coercion=False`.\n  When `allow_coercion=True` the annotations are treated as `As` patterns\n  allowing the values to be coerced to the given type. When\n  `allow_coercion=False` the annotations are treated as `Is` patterns and\n  the values must be exactly of the given type. The default is\n  `allow_coercion=True`.\n- Annotable objects support inheritance, the annotations are inherited\n  from the base classes and the signatures are merged providing a\n  seamless experience.\n- Annotable objects can be called with either or both positional and\n  keyword arguments, the positional arguments are matched to the\n  annotations in order and the keyword arguments are matched to the\n  annotations by name.\n\n```py\nfrom typing import Optional\nfrom koerce import Annotable\n\nclass MyBase(Annotable):\n    x: int\n    y: float\n    z: Optional[str] = None\n\nclass MyClass(MyBase):\n    a: str\n    b: bytes\n    c: tuple[str, ...] = (\"a\", \"b\")\n    x: int = 1\n\n\nprint(MyClass.__signature__)\n# (y: float, a: str, b: bytes, c: tuple = ('a', 'b'), x: int = 1, z: Optional[str] = None)\n\nprint(MyClass(2.0, \"a\", b\"b\"))\n# MyClass(y=2.0, a='a', b=b'b', c=('a', 'b'), x=1, z=None)\n\nprint(MyClass(2.0, \"a\", b\"b\", c=(\"c\", \"d\")))\n# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=1, z=None)\n\nprint(MyClass(2.0, \"a\", b\"b\", c=(\"c\", \"d\"), x=2))\n# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=2, z=None)\n\nprint(MyClass(2.0, \"a\", b\"b\", c=(\"c\", \"d\"), x=2, z=\"z\"))\n# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=2, z='z')\n\nMyClass()\n# TypeError: missing a required argument: 'y'\n\nMyClass(2.0, \"a\", b\"b\", c=(\"c\", \"d\"), x=2, z=\"z\", invalid=\"invalid\")\n# TypeError: got an unexpected keyword argument 'invalid'\n\nMyClass(2.0, \"a\", b\"b\", c=(\"c\", \"d\"), x=2, z=\"z\", y=3.0)\n# TypeError: multiple values for argument 'y'\n\nMyClass(\"asd\", \"a\", b\"b\")\n# ValidationError\n```\n\n## Performance\n\n`koerce`'s performance is at least comparable to `pydantic`'s performance.\n`pydantic-core` is written in rust using the `PyO3` bindings making it\na pretty performant library. There is a quicker validation / serialization\nlibrary from `Jim Crist-Harif` called [msgspec](https://github.com/jcrist/msgspec)\nimplemented in hand-crafted C directly using python's C API.\n\n`koerce` is not exactly like `pydantic` or `msgpec` but they are good\ncandidates to benchmark against:\n\n```\nkoerce/tests/test_y.py::test_pydantic PASSED\nkoerce/tests/test_y.py::test_msgspec PASSED\nkoerce/tests/test_y.py::test_annotated PASSED\n\n\n------------------------------------------------------------------------------------------- benchmark: 3 tests ------------------------------------------------------------------------------------------\nName (time in ns)            Min                   Max                  Mean              StdDev                Median                IQR            Outliers  OPS (Kops/s)            Rounds  Iterations\n---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\ntest_msgspec            230.2801 (1.0)      6,481.4200 (1.60)       252.1706 (1.0)       97.0572 (1.0)        238.1600 (1.0)       5.0002 (1.0)      485;1616    3,965.5694 (1.0)       20000          50\ntest_annotated          525.6401 (2.28)     4,038.5600 (1.0)        577.7090 (2.29)     132.9966 (1.37)       553.9799 (2.33)     34.9300 (6.99)      662;671    1,730.9752 (0.44)      20000          50\ntest_pydantic         1,185.0201 (5.15)     6,027.9400 (1.49)     1,349.1259 (5.35)     320.3790 (3.30)     1,278.5601 (5.37)     75.5100 (15.10)   1071;1424      741.2206 (0.19)      20000          50\n```\n\nI tried to used the most performant API of both `msgspec` and `pydantic`\nreceiving the arguments as a dictionary.\n\nI am planning to make more thorough comparisons, but the model-like\nannotation API of `koerce` is roughly twice as fast as `pydantic` but\nhalf as fast as `msgspec`. Considering the implementations it also\nmakes sense, `PyO3` possible has a higher overhead than `Cython` has\nbut neither of those can match the performance of hand crafted python\n`C-API` code.\n\nThis performance result could be slightly improved but has two huge\nadvantage of the other two libraries:\n1. It is implemented in pure python with cython decorators, so it\n   can be used even without compiling it. It could also enable\n   JIT compilers like PyPy or the new copy and patch JIT compiler\n   coming with CPython 3.13 to optimize hot paths better.\n2. Development an be done in pure python make it much easier to\n   contribute to. No one needs to learn Rust or python's C API\n   in order to fix bugs or contribute new features.\n\n## TODO:\n\nThe README is under construction, planning to improve it:\n- [ ] Example of validating functions by using @annotated decorator\n- [ ] Explain `allow_coercible` flag\n- [ ] Proper error messages for each pattern\n\n## Development\n\n- The project uses `poetry` for dependency management and packaging.\n- Python version support follows https://numpy.org/neps/nep-0029-deprecation_policy.html\n- The wheels are built using `cibuildwheel` project.\n- The implementation is in pure python with cython annotations.\n- The project uses `ruff` for code formatting.\n- The project uses `pytest` for testing.\n\nMore detailed developer guide is coming soon.\n\n## References\n\nThe project was mostly inspired by the following projects:\n- https://github.com/scravy/awesome-pattern-matching\n- https://github.com/HPAC/matchpy\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkszucs%2Fkoerce","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkszucs%2Fkoerce","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkszucs%2Fkoerce/lists"}