Skip to main content

Context-aware conversion between structured and unstructured Python data using multidispatch

Project description

PyPI CI codecov Ruff License: MIT

Codify

Codify is a type-driven, context-aware data conversion framework. Conversion rules depend not just on a value's type but also on where it sits in the object graph, expressed declaratively. It ships with default behavior for converting unstructured data (e.g., a JSON dict) to structured data (e.g., a dataclass or TypedDict), and back.

Installation

pip install codify-conv
import codify

Rationale

Why codify?

The primary focus is the context-aware hook system: target type, root type (the conversion entry), owner type, and location in the object graph are all available as axes for registering hooks declaratively. A second characteristic is clean, non-intrusive separation of conversion logic from the domain model. Codify does not require you to extend a library-specific base class or embed validation and transformation logic in the domain model. In this way, codify serves as a bridge between data models of different layers.

This is especially useful when you need type-driven data mapping across heterogeneous or evolving sources. Domain models are usually stable, and sometimes you have no control over them at all (e.g., when they are owned or provided by a third party). Yet you may still need to transform heterogeneous or evolving external data into those models. Codify keeps that friction low.

# The problem: Same type, different conversion depending on the context.

# By root type, expand department codes when exporting
@register
def structure_hook(ctx: Ctx[Employee, Under[ExportHRReport]], data: str) -> Employee:
    ...

# By owner type, only name is erased; codify handles the rest
@register
def structure_hook(ctx: Ctx[Employee, Of[Anonymized]], data: str) -> Employee:
    ...

# By location, legacy format in a specific field
@register
def structure_hook(ctx: Ctx[Employee, "$.managed_in_old_db[?]"], data: str) -> Employee:
    ...

See Examples for the full development of this example.

Why not codify?

Codify is not the fastest option. For most IO-bound applications the overhead is negligible, but if maximum throughput is a hard requirement, codify may not be the right choice. Codify also focuses narrowly on data conversion and does not provide features like JSON schema generation or standalone runtime validation outside the conversion process.

Examples

The examples build up progressively. If you want something interesting first, start from the Root type example instead of the Basic usage below.

Basic usage

Let's start with a basic example. Codify provides structure and unstructure functions, following the conventions of cattrs.

from codify import structure, unstructure
from dataclasses import dataclass

@dataclass
class Employee:
    name: str
    department: str

records = [
    {"name": "jack", "department": "data"},
    {"name": "jane", "department": "sales"}
]

structured = structure(list[Employee], records)
# [Employee('jack', 'data'), Employee('jane', 'sales')]

unstructure(list[Employee], structured)
# == records

structure(Employee, {"name": "jade"})
# MissingFields exception raised.

This demonstrates codify's default behavior of structuring a dict into a dataclass. Codify supports TypedDict, Literal, NewType, union, etc. See Default behavior for the supported types out of the box.

User-defined hooks

Suppose that the preferred representation of an employee in unstructured data is a string "name@department" instead of a dict. We can write a hook to support the representation.

from codify import register, Ctx, ValidationError

# A helper to parse the string representation
def parse_employee(data: str) -> tuple[str, str]:
    splits = data.split("@")
    if len(splits) != 2 or not all(splits):
        raise ValueError("Invalid employee format")
    return splits[0], splits[1]

# A hook to structure the string representation
@register
def structure_hook(ctx: Ctx[Employee], data: str) -> Employee:
    try:
        name, department = parse_employee(data)
        return Employee(name, department)
    except ValueError as e:
        raise ValidationError(ctx, data, str(e)) from e

# A hook to unstructure as the string representation
@register
def unstructure_hook(ctx: Ctx[Employee], data: Employee) -> str:
    return f"{data.name}@{data.department}"

records = ["jack@data", "jane@sales"]

structured = structure(list[Employee], records)
# [Employee('jack', 'data'), Employee('jane', 'sales')]

unstructure(list[Employee], structured)
# ['jack@data', 'jane@sales']

structure(list[Employee], ["john:infra"])
# ValidationError: Invalid employee format (at $[0])

# Dict representation still works
records = [
    {"name": "jack", "department": "data"},
    {"name": "jane", "department": "sales"}
]

structure(list[Employee], records)
# [Employee('jack', 'data'), Employee('jane', 'sales')]

Note that the dict representation still works. If we want to forbid it, we can register a hook with data: dict and raise an exception. Hooks take priority over default handlers.

The codify.register decorator registers hooks. Hook names are fixed as structure_hook and unstructure_hook. Any number of hooks can be registered under these two fixed names. Codify's hook system is built on top of its own internal multidispatch ("overloading" dispatched by runtime type) system. This is like registering multiple versions of structure_hook or unstructure_hook with different argument signatures.

At runtime, ctx represents the current context. The hook body can use properties of ctx and utility functions working with ctx to inspect the current context. They are also useful for debugging. See Ctx for details.

Root type

Sometimes the same domain type needs different conversion depending on what's being converted overall. Suppose the HR system stores departments as short codes internally but expands them to full names when exporting employee records to external consumers.

Under[Root] selects by the root of the conversion entry — the type passed to structure or unstructure.

from codify import Under, structure_by_type
from dataclasses import replace

@dataclass
class HRReport:
    employees: list[Employee]

@dataclass
class ExportHRReport:
    employees: list[Employee]

DEPT_NAMES = {"ENG": "Engineering", "SAL": "Sales", "DAT": "Data Science"}

# Fires only when the conversion was entered at ExportHRReport.
# Inside an HRReport, the previously-registered Ctx[Employee] hook runs unchanged.
@register
def structure_hook(ctx: Ctx[Employee, Under[ExportHRReport]], data: str) -> Employee:
    # Structure with only target type, stripping owner type, root type, and path.
    # The hook defined in the previous example is called.
    employee = structure_by_type(ctx, data)
    return replace(
        employee,
        department=DEPT_NAMES.get(employee.department, employee.department)
    )

records = ["jack@ENG", "jane@SAL"]

structure(HRReport, {"employees": records})
# HRReport(employees=[Employee("jack", "ENG"), Employee("jane", "SAL")])

structure(ExportHRReport, {"employees": records})
# ExportHRReport(employees=[Employee("jack", "Engineering"), Employee("jane", "Sales")])

structure_by_type(ctx, data) strips all contextual information except the structured type and runs a hook registered with only the target type, or the default handler if no such hook exists. Here it falls through to the Ctx[Employee] hook from the previous section, after which we apply the expansion. unstructure_by_type is also available. Note that child contexts (for fields or elements) carry owner type information, while root type and path remain suppressed.

Under composes with the other axes; you can combine it freely with target type, owner, and path — e.g., Ctx[Employee, Of[Department], Under[ExportHRReport]] to scope further.

Note that if there are multiple hooks matching the context, a hook registered with the most specific context wins over less specific ones. The hook defined in this example (with Ctx[Employee, Under[ExportHRReport]]) is more specific than the previous one (with Ctx[Employee]). So if the root is ExportHRReport, the hook in this example is fired. See Hook resolution and Conflict resolution for details of the hook dispatch.

Owner type

Now assume that some downstream tasks want anonymized data to protect private information. The downstream task API has wrapper types Anonymized[T] and Transparent[T] to mark whether the contents should be anonymized.

In this example, we use the Of parameter to express the owner type.

from codify import Of, structure_by_type
from dataclasses import replace

@dataclass
class Anonymized[T]:
    person: T

@dataclass
class Transparent[T]:
    person: T

# This hook works on both string and dict representations.
@register
def structure_hook(ctx: Ctx[Employee, Of[Anonymized[Employee]]], data: str | dict) -> Employee:
    employee = structure_by_type(ctx, data)
    return replace(employee, name="")

record = {"person": "jack@frontend"}

structure(Anonymized[Employee], record)
# Anonymized(Employee('', 'frontend'))

structure(Transparent[Employee], record)
# Transparent(Employee('jack', 'frontend'))

record_in_dict = {"person": {"name": "jack", "department": "frontend"}}

structure(Anonymized[Employee], record_in_dict)
# Anonymized(Employee('', 'frontend'))

structure(Transparent[Employee], record_in_dict)
# Transparent(Employee('jack', 'frontend'))

Note that we could instead write Ctx[Anonymized[Employee]] to fire on the wrapper itself. Both forms are valid; the choice depends on what we want to control. A Ctx[Anonymized[Employee]] hook takes ownership of the whole wrapper construction (extracting person, building the Anonymized instance, and so on). This is useful when we want the hook to reshape the wrapper itself, e.g., change its field layout or emit a different result type. The Of form above instead lets codify's default wrapper handling stand and overrides only the inner Employee transformation, keeping the hook focused on the anonymization logic and unaffected by (future) changes in Anonymized's field set.

Compared to the Root type example above, Of selects by the directly surrounding container at this position, while Under selects by the type at the top of the conversion. Use Of when a wrapper type around the value tells you what to do; use Under when the mode is decided once at the entry point and applies everywhere underneath.

Scoped hooks

What if some downstream tasks need fake unique names instead of erased names? We can use scoped hooks.

import uuid
from codify import codify_config

# This copy is independent of the original `register` but has already registered hooks.
register_with_random_uuid_name = register.copy()

# Now this overrides the Of-anonymization hook from the Owner type example, but only in the scope of `register_with_random_uuid_name`.
@register_with_random_uuid_name
def structure_hook(ctx: Ctx[Employee, Of[Anonymized[Employee]]], data: str | dict) -> Employee:
    employee = structure_by_type(ctx, data)
    return replace(employee, name=str(uuid.uuid4()))

# Apply the scope
with codify_config(dispatcher=register_with_random_uuid_name):
    structure(Anonymized[Employee], record)
    # Anonymized(Employee('3dc46dd4-22f2-...', 'frontend'))

# The global scope
structure(Anonymized[Employee], record)
# Anonymized(Employee('', 'frontend'))

Path-aware hooks

Suppose that employee information in a specific path has a different format "name:department". We can register hooks with JSONPath-like path expressions.

This example demonstrates a targeted fix for a messy real-world situation. Such fixes are usually ad hoc. However, codify keeps it declarative and scoped — the model stays clean, and the format quirk doesn't leak beyond its hook.

@dataclass
class EmployeeRegistry:
    managed_in_new_db: list[Employee]
    managed_in_old_db: list[Employee]

@register
def structure_hook(ctx: Ctx[Employee, "$.managed_in_old_db[?]"], data: str) -> Employee:
    parsed = data.split(":")
    if len(parsed) != 2 or not all(parsed):
        raise ValidationError(ctx, data, "Invalid legacy employee format")
    return Employee(*parsed)

records = {
    "managed_in_new_db": ["jack@data", "jane@sales"],
    "managed_in_old_db": ["john:infra", "joel:infra"]
}
structure(EmployeeRegistry, records)
# EmployeeRegistry(
#     [Employee("jack", "data"), Employee("jane", "sales")]
#     [Employee("john", "infra"), Employee("joel", "infra")]
# )

The path expression also supports rootless location. For example, Ctx[".foo"] means a field foo of whatever dataclass or TypedDict in any location. See also Hooks for a single field or item and Path expression.

Pre- and post-validation

Suppose that we need to reject list processing if the list is too long.

from codify import structure_default

@register
def structure_hook(ctx: Ctx[list[Employee]], data: list[Any]) -> list[Employee]:
    if len(data) > 100:
        raise ValidationError(ctx, data, "Too large data")
    return structure_default(ctx, data)

structure(list[Employee], records * 100)
# ValidationError: Too large data (at $)

Forwarding to structure_default runs the default handler. You can use it for pre- and post-validation: validate the data, call structure_default, then validate the result — or modify and return it. It bypasses any hooks for the current context; hooks for children (fields or elements) still fire.

You may be tempted to write data: list[str] instead of data: list[Any]. However, this raises an error at registration time due to Python's limited runtime generic type information. You can use list[str] as a parameter of Ctx, but not as a top-level hint on the data side. See Restrictions on data type for details.

Renamed fields

Sometimes the fields of a target dataclass (or TypedDict) and the keys of the input data do not match. One option is to call structure_default with a modified copy of data inside a hook, but that can be inefficient. structure_default and unstructure_default accept an optional keymap: dict[str, str] argument. keymap maps structured keys (field names) to unstructured keys (dict keys). For any field not in keymap, the input data must use the field name as its key.

It is strongly recommended to define keymap as a module-level constant. Defining it as a local variable creates a new dict on every call — wasteful on its own, and it also defeats codify's internal cache.

In this example, we use keymap to process legacy database entries having "division" instead of "department".

from codify import structure_default, unstructure_default

keymap = {"department": "division"}

@register
def structure_hook(ctx: Ctx[Employee, "$.managed_in_old_db[?]"], data: dict) -> Employee:
    return structure_default(ctx, data, keymap=keymap)

@register
def unstructure_hook(ctx: Ctx[Employee, "$.managed_in_old_db[?]"], data: Employee) -> dict:
    return unstructure_default(ctx, data, keymap=keymap)

records = {
    "managed_in_new_db": [
        {"name": "jane", "department": "sales"},
    ],
    "managed_in_old_db": [
        {"name": "john", "division": "infra"},
        {"name": "joel", "division": "infra"}
    ]
}
structured = structure(EmployeeRegistry, records)
# EmployeeRegistry(
#     [Employee('jane', 'sales')],
#     [Employee("john", "infra"), Employee("joel", "infra")]
# )
unstructure(EmployeeRegistry, structured)
# == records

Injecting extra data

You may want to thread extra data through the transformation process and use it in hooks — for example, to inject dependencies or control hook behavior. Call structure or unstructure with the optional extra=your_extra_data argument and read it via get_extra(ctx) inside hooks. get_extra returns the same object you passed; mutate it directly to share state across hooks (e.g., pass extra={} and mutate the dict).

from typing import Any
from codify import get_extra

@register
def structure_hook(ctx: Ctx[Employee], data: str) -> Employee:
    try:
        name, department = parse_employee(data)
        if get_extra(ctx)["mask-name"]:
            name = "*"
        return Employee(name, department)
    except ValueError as e:
        raise ValidationError(ctx, data, str(e)) from e

records = [
    ["jack@data", "jane@sales"],
    ["john@infra"]
]

structure(list[list[Employee]], records, extra={"mask-name": False})
# [
#     [Employee('jack', 'data'), Employee('jane', 'sales')],
#     [Employee('john', 'infra')]
# ]

structure(list[list[Employee]], records, extra={"mask-name": True})
# [
#     [Employee('*', 'data'), Employee('*', 'sales')],
#     [Employee('*', 'infra')]
# ]

Hooks for a single field or item

Ctx[".foo"] means a field foo. Such a hook fires for any field named foo of a dataclass or a TypedDict. We can also specify the owner type, like Ctx[".foo", Of[Foo]] where Foo is a dataclass or a TypedDict. Such a hook fires only for Foo.foo. The field type can also be constrained, like Ctx[".foo", int]. Such a hook fires if the field is foo and its type is int. We can use Ctx[".?", Of[Foo]] to denote any field of Foo.

You can also use Ctx["[0]"] to denote the first element of any Sequence, or a dict value at integer key 0. The type and owner type can be constrained here as well.

In this example, we use Ctx[".?", Of[Foo]] to validate each field of Foo in a uniform way. Suppose that validation logic is given as field metadata (say, by a third party) and we must use it. Note, however, that codify does not require field metadata in any way.

# This example is independent of the previous ones. So we import all necessary names here.
from codify import register, structure, structure_default, Ctx, Of, ValidationError
from dataclasses import dataclass, field
from typing import Any

@dataclass
class User:
    name: str = field(metadata={
        "validator": lambda x: None if len(x) > 3 else "Too short name"
    })
    age: int = field(metadata={
        "validator": lambda x: None if x >= 0 else "Negative age"
    })

# Here the rootless path expression ".?" means any field.
@register
def structure_hook(ctx: Ctx[".?", Of[User]], data: Any) -> Any:
    if validator := ctx.field.metadata.get("validator"):
        error = validator(data)
        if error:
            raise ValidationError(ctx, data, error)
    return structure_default(ctx, data)

structure(User, {"name": "jack", "age": 20})
# User('jack', 20)

structure(User, {"name": "ja", "age": 20})
# ValidationError: Too short name (at $.name)

structure(User, {"name": "jack", "age": -1})
# ValidationError: Negative age (at $.age)

Note also that __post_init__, which may have post-validation logic, works as expected transparently.

If we register an additional hook with more specific context, it has higher priority than the one in the example. For example, if a new hook with Ctx[".name", Of[User]] is registered, it is fired instead of the one in the example. So, the new one should implement its own validation.

Reading root and sibling data

A hook sometimes needs context from outside its current position — typically, root-level data that drives behavior deep in the tree.

# This example is independent of the previous ones. So we import all necessary names here.
from codify import Ctx, Of, get_data, get_root, register, structure
from dataclasses import dataclass
from typing import Literal

@dataclass
class Section:
    title: str
    body: str

@dataclass
class Report:
    mode: Literal["summary", "full"]
    sections: list[Section]

# Body content is truncated when the whole report is in summary mode.
@register
def structure_hook(ctx: Ctx[".body", Of[Section]], data: str) -> str:
    root_data = get_data(get_root(ctx))
    if root_data["mode"] == "summary":
        return data[:80] + "…"
    else:
        return data

body = "We had a strong quarter across all regions. Revenue grew 12% year over year, driven by enterprise renewals and faster onboarding."
structure(Report, {"mode": "summary", "sections": [{"title": "Q1", "body": body}]})
# Report(mode="summary", sections=[Section(title="Q1", body="We had a strong quarter across all regions. Revenue grew 12% year over year, dri…")])

structure(Report, {"mode": "full", "sections": [{"title": "Q1", "body": body}]})
# Report(mode="full", sections=[Section(title="Q1", body="We had a strong quarter across all regions. Revenue grew 12% year over year, driven by enterprise renewals and faster onboarding.")])

get_data(ctx.parent) is also available for sibling access under the same parent. See Context for available information via ctx.

If mode were instead a type-level distinction (e.g., a SummaryReport vs FullReport wrapper type), Root type would be the right tool — Under[SummaryReport] selects by the entry type, not by a runtime field value. Use get_data(get_root(ctx)) when the discriminating value is part of the data itself.

Union discrimination

When a union member is a dataclass or TypedDict, codify discriminates members automatically. Two mechanisms apply, and they compose:

  • Tag-based: a field declared as Literal[<single_value>] acts as a discriminator tag when each member declares a different literal value for it.
  • Field-based: which required fields are present (and which fields declared by other members are absent) selects the matching member.

After discrimination, codify checks for a hook matching the current context with the union type replaced by the chosen member type; if one exists, it fires.

See structure and unstructure for more details on union discrimination.

# This example is independent of the previous ones. So we import all necessary names here.
from codify import structure
from dataclasses import dataclass
from typing import Literal

@dataclass
class TextMessage:
    type: Literal["text"]
    content: str

@dataclass
class ImageMessage:
    type: Literal["image"]
    url: str

@dataclass
class Error:
    code: int
    message: str

Event = TextMessage | ImageMessage | Error

# Tag-based: picked by the `type` field value.
structure(Event, {"type": "text", "content": "hi"})
# TextMessage(type='text', content='hi')

structure(Event, {"type": "image", "url": "http://x"})
# ImageMessage(type='image', url='http://x')

# Field-based: Error has no `type` field, so it's picked by the presence of its own required fields (`code`, `message`).
structure(Event, {"code": 500, "message": "boom"})
# Error(code=500, message='boom')

# Unknown tag value matches no member.
structure(Event, {"type": "video", "content": "x"})
# NoStructureHook: The data is not compatible with any member of the union (at $)

# Carrying fields exclusive to another member is rejected.
structure(Event, {"type": "text", "content": "hi", "code": 1})
# NoStructureHook: The data is not compatible with any member of the union (at $)

If members cannot be distinguished — same fields, no usable tag — structure raises AmbiguousUnion at conversion time.

If you need to implement your own discrimination strategy, you can write a hook matching the union.

@register
def structure_hook(ctx: Ctx[Event], data: dict) -> Event:
    ...

Thread safety

Scoped hooks are also the mechanism for thread safety: use a separate dispatcher per thread via register.copy() and codify_config.

A dispatcher can run only one structure or unstructure call at a time. Calling structure or unstructure again from inside a hook raises ReentranceError. Inside hooks, use structure_default, unstructure_default, structure_by_type, or unstructure_by_type to delegate to codify.

from threading import Thread

@register
def structure_hook(ctx: Ctx[Employee], data: str) -> Employee:
    name, department = parse_employee(data)
    return Employee(name, department)

def run_in_thread(record: str, out: list[Employee]) -> None:
    thread_register = register.copy()
    with codify_config(dispatcher=thread_register):
        out.append(structure(Employee, record))

employees = []
threads = [
    Thread(target=run_in_thread, args=("jack@data", employees)),
    Thread(target=run_in_thread, args=("jane@sales", employees)),
]

for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

General multidispatch

Codify's hook system is built on top of its own internal multidispatch system. You don't need prior knowledge of the multidispatch system to use codify — the codify.register decorator covers everyday hook registration. However, the multidispatch system can also be used as a general-purpose dispatcher beyond codify's hooks. See Multidispatch for details.

from dataclasses import dataclass
from typing import Literal

from codify import Ctx, register, structure

@dataclass
class Espresso:
    shots: int

@dataclass
class Latte:
    shots: int

@dataclass
class Americano:
    shots: int

@dataclass
class Mocha:
    shots: int

Drink = Latte | Americano | Mocha

# General multidispatch: (Espresso, add-in tag) → specific drink
@register
def brew(base: Espresso, add: Literal["milk"]) -> Latte:
    return Latte(base.shots)

@register
def brew(base: Espresso, add: Literal["water"]) -> Americano:
    return Americano(base.shots)

@register
def brew(base: Espresso, add: Literal["chocolate"]) -> Mocha:
    return Mocha(base.shots)

# Codify hook: delegate pairing logic to brew
@register
def structure_hook(ctx: Ctx[Drink], data: dict) -> Drink:
    base = structure(Espresso, data["base"])
    return brew(base, data["add"])

structure(Drink, {"base": {"shots": 2}, "add": "milk"})       # Latte(shots=2)
structure(Drink, {"base": {"shots": 1}, "add": "chocolate"})  # Mocha(shots=1)
structure(Drink, {"base": {"shots": 3}, "add": "water"})      # Americano(shots=3)

Default behavior

structure

structure(target_type, data, extra=None)
  • target_type: The type of the structured data.
  • data: The data to be structured.
  • extra: Extra data to be used by user-defined hooks. See also Injecting extra data.

If target_type is Any, the default handler returns data as-is. The same applies to any field or element declared as Any. However, hook dispatch still runs at those positions. Hooks matching on owner type (Of), root type (Under), or path can fire regardless of the Any declaration.

Target types supported by default

Note that these are target types (the target_type argument or declared types of fields/elements), not source data types.

Scalar types

  • bool, int, float, str:
    • If the input is the same type, returned as-is.
    • If the input is a subtype, converted to the target type. Note that bool, int, float are treated as forming a subtype hierarchy, even though issubclass(int, float) returns False.
    • int to bool coercion is supported.
    • No coercion to str. You need to explicitly write a hook to convert strings to numbers or vice versa.
  • Enum:
    • Conversion from any value matching an Enum member's .value. Raises ValidationError if no member matches.
  • Decimal:
    • Conversion from str.
    • Note that conversion from int or float is not supported by default. Register a hook if needed.
  • bytes:
    • Conversion from base64 encoded str.
  • Path:
    • Conversion from str.
  • UUID:
    • Conversion from str.
  • date, datetime:
    • Conversion from ISO-format str.

Composite types

  • list[T], set[T], frozenset[T], Sequence[T], Collection[T]:
    • Conversion from any of list, tuple, set, frozenset to any of them is supported. Sequence[T] and Collection[T] targets produce a list.
    • Elements are converted with T as target type. Conversion from list and tuple preserves the index.
    • For bare collection types (e.g., list), the element type is regarded as Any.
  • tuple[T1, T2, ...]:
    • Fixed-length tuple with one declared type per position. Each position is converted by its declared type.
    • The variadic form tuple[T, ...] (and bare tuple) is treated homogeneously, with all elements converted as T (or Any for bare).
  • dict[K, V]:
    • Conversion from dict is supported. Both key and value are converted.
    • dict is regarded as dict[Any, Any]. The single-arg dict[K] is not supported.
  • TypedDict:
    • Conversion from dict is supported.
    • Each declared field is converted from the source value of the same key, according to declared types.
    • Optionality follows total= and Required[] / NotRequired[]. Missing required keys raise MissingFields.
    • PEP 728 (planned for Python 3.15, available via typing_extensions): with closed=True, extra keys raise ExtraFields. With extra_items=T, extra keys are kept and each value is structured as T. Otherwise (the default), extra keys are silently dropped. closed / extra_items declared on an ancestor TypedDict is inherited.
  • Dataclass:
    • Conversion from dict is supported.
    • Each declared field is converted from the source value of the same key, according to declared types.
    • Default value and factory are supported.
    • If source dict is missing required fields or has extra fields, MissingFields or ExtraFields is raised. This is stricter than TypedDict's default, which drops extras silently.

Derived types

  • Union:
    • Any union of the supported types is also supported as a target type.
    • Dataclasses and TypedDicts are discriminated by the source dict's field set and/or by Literal[<single_value>] tag fields with distinct values (tagged union); the two mechanisms compose, so different subsets may be split by different tag fields. The source dict must carry any tag that's used, even if the Python-side field has a default. If the members cannot be distinguished, AmbiguousUnion is raised. See Union discrimination for examples.
    • For other types, the member is selected by the following priority. At each step, a member is only eligible if it is constructable from the data — i.e., a hook or default handler exists for the data's runtime type and the member type. (1) If a user-registered hook matches exactly one member for the data's runtime type, that member is selected. (2) Otherwise, if the data's runtime type matches exactly one member, that member is selected. See Restrictions on data type for some caveats on the match. (3) Otherwise, the member is selected by checking whether a default handler is registered for the member and the data's runtime type; if exactly one matches, it is selected. If at any step multiple eligible candidates remain, AmbiguousUnion is raised.
    • The discriminated member becomes the dispatch target, so a hook registered for the current context (the discriminated member type, owner, root, path) fires as usual.
    • If you need custom union resolution, you can register a hook for the union type. See Hook resolution for details.
  • Literal:
    • Accepts only values equal to one of the declared literal constants. Types also should be identical, for example, Literal[0] does not accept False. The accepted value is returned as-is. Raises ValidationError otherwise.
    • Literal[a, b, ···] is equivalent to Literal[a] | Literal[b] | ···.
    • A hook on Ctx[int] will not catch Literal[1]; register for Ctx[Literal[1]] instead.
  • NewType:
    • Default handler for the supertype is called. For example, Int = NewType("Int", int) is treated as target type int. However, a hook for int does not fire unless it is registered with @register.ctx_subtypes. You can register a hook with Ctx[Int]. The hook fires only for Int, not int.
  • Type alias:
    • type X = Y is regarded as transparent aliasing. X is exactly the same as Y.

unstructure

unstructure(declared_type, data, extra=None)
  • declared_type: The type of the structured data.
  • data: The data to be unstructured.
  • extra: Extra data to be used by user-defined hooks. See also Injecting extra data.

declared_type is respected — not the runtime type of data. For example, let derived be an instance of Derived, a subclass of Base. unstructure(Base, derived) drops any fields that Derived adds beyond Base. The same "slicing" happens when the declared type of a field or element is Base but the runtime value is Derived.

Another example is the union discrimination. When the declared type is a union, a single member is discriminated because one specific declared type is needed for hook resolution and recursive unstructuring of fields or elements.

This is important also for hook resolution. Let's suppose that Foo is a TypedDict class. You can register two hooks unstructure_hook(ctx: Ctx[Foo], data: dict) and unstructure_hook(ctx: Ctx[dict], data: dict). If the current data is declared as Foo (e.g., a field declared as Foo or an element of list[Foo]), the former hook fires. If the current data is declared as dict, the latter hook fires.

Unlike structure, unstructure does not verify data against declared_type — it follows the declared shape and reads only what that shape calls for. For instance, closed=True on a TypedDict raises on structure but is a no-op on unstructure (extras are simply not emitted). However, it assumes the runtime type is compatible with the declared type. If the runtime type of a data is not compatible to its declared type (e.g., float value assigned to a field declared as int), the default behavior is not defined.

If the declared type is Any, however, codify respects the runtime type. For example, unstructure(Any, derived) includes all of derived's fields in the result. Any is propagated to children, so they are unstructured as Any too. Note that user hooks registered against specific target types do not fire at Any-typed positions; only Ctx[Any] hooks (and hooks matching by owner type, root type, or path) match.

Declared types supported by default

The defaults broadly mirror the structure side, applied in reverse. Again note that these are declared types (the declared_type argument or declared types of fields/elements), not source data types.

Scalar types

  • bool, int, float, str:
    • Returned as-is when the runtime type matches.
    • Subtypes are converted upward in the same hierarchy as structure (bool → int → float). For example, unstructure(int, True) returns 1.
  • Enum:
    • Unstructured as the member's .value. When used as a dict key, stringified to str(member.value).
  • Decimal, Path, UUID:
    • str(value).
  • bytes:
    • base64-encoded str.
  • date, datetime:
    • ISO-format str via .isoformat().

Composite types

  • list[T], set[T], frozenset[T], Sequence[T]:
    • Unstructured to list. Elements are unstructured as T. For bare types (e.g., list), elements are unstructured as Any.
  • tuple[T1, T2, ...]:
    • Fixed-length tuple with one declared type per position. Unstructured to list; each position is unstructured by its declared type.
    • The variadic form tuple[T, ...] (and bare tuple) is treated homogeneously, with all elements unstructured as T (or Any for bare).
  • dict[K, V]:
    • Unstructured to dict. Each key is unstructured first; the result must be str, otherwise ValidationError is raised. Integer and Enum keys are stringified automatically.
  • TypedDict:
    • Unstructured to dict. Each declared field is unstructured according to its declared type.
    • With extra_items=T, extra runtime keys are kept and unstructured as T. Otherwise extra runtime keys are silently dropped.
  • Dataclass:
    • Unstructured to dict. Each declared field is unstructured. Slicing applies as described above.

Derived types

  • Union:
    • TypedDicts in the union are discriminated the same way as in structure — by field set and/or Literal[<single_value>] tag fields (tagged union).
    • For non-TypedDict members, the matching member is selected by the same runtime-type and constructability rules used for non-dataclass unions in structure. Dataclasses are also selected by runtime type.
    • Ambiguity raises AmbiguousUnion.
    • The selected member becomes the dispatch target, so a hook registered on the member type fires as usual.
  • Literal:
    • The default handler unstructures the value by its runtime type (e.g., Literal[1, 2]int, Literal["a", "b"]str), without verifying that the value belongs to the literal set. Register a hook for Ctx[Literal[...]] to override.
  • NewType:
    • Unstructured as the supertype. A hook for the supertype does not fire unless it is registered with @register.ctx_subtypes.
  • Type alias:
    • type X = Y is transparent; X is exactly the same as Y.

When the declared type is Any, the runtime type drives dispatch and Any is propagated to children:

  • Primitives are returned as-is.
  • Path, UUID, Decimal: str(value).
  • bytes: base64-encoded str.
  • date, datetime: ISO-format str.
  • Lists, tuples, sets, frozensets, dicts, and dataclass instances are recursed into; each child carries Any.

Defining hooks

Codify first tries to apply a hook for the current context and data. If no hook matches the current context and data, it tries a default handler. So, a hook can override the default behavior or coexist with defaults.

Hook names are fixed as structure_hook and unstructure_hook. You cannot use arbitrary names.

The hook signature declares when the hook should fire (if it wins the competition with other hooks), not what context information it is interested in. The ctx argument always carries full context information. See [Ctx] for the available information.

structure_hook

from codify import register, Ctx

@register
def structure_hook(ctx: Ctx[...], data: T):
    ...

This defines a structure hook converting an input value of type T.

The Ctx type declares in which context this hook fires. The context is a position in the (output) structured object graph, not in the (input) unstructured object graph. See Context and Hook resolution. You can set the return type but it is not used by codify. Note that you cannot use an arbitrary function name; it must be structure_hook.

T can be a union, Literal, type alias, user-defined (fully parameterized) generic type, bare built-in collection type (like list, set, ...), or built-in collection type parameterized with Any (like list[Any], set[Any], ...). See Restrictions on data type for why parameterized built-in collections (like list[int]), types defined with NewType, and TypedDict classes are not supported as a data type hint.

unstructure_hook

from codify import register, Ctx

@register
def unstructure_hook(ctx: Ctx[...], data: T):
    ...

This defines an unstructure hook converting an input value of type T. Almost the same as structure_hook except that unstructure_hook is fired during unstructuring.

Manual delegation

Utility to call default handlers or other hooks manually.

structure_default(ctx, data, keymap=None) / unstructure_default(ctx, data, keymap=None):

Call the default handler, bypassing any hook. Note that hooks are not bypassed for fields or elements of data. See Pre- and post-validation for an example.

keymap: dict[str, str] is used to map field names to dict keys for dataclasses or TypedDict. keymap can contain only renamed fields. keymap=None means that all field names are identical to the dict keys. For structure_default, the input dict must use the mapped key. For unstructure_default, the output dict carries the mapped key. In either case, the original field name is not used. keymap should be defined as a module-level constant and not be mutated. A local variable creates a new dict object on every call, defeating the internal cache. keymap is used only for structuring or unstructuring dataclasses and TypedDicts. For other non-union types, it is ignored. Passing keymap to default conversion for a union raises ValidationError; register a hook for the concrete member type instead. See Renamed fields for an example.

structure_by_type(ctx, data) / unstructure_by_type(ctx, data):

Call the default handler or hooks registered with only the target type. Hooks requiring root type, owner type, or path conditions are ignored for the current position. For fields or elements within the current value, the owner type is available, but root type and path remain suppressed. This is useful when a more-specific hook (one with Under, Of, or a path) needs to delegate to the less-specific hook or built-in default for the same type, then adjust the result. See Root type for an example.

We can also call structure or unstructure with a different target type or modified data. However, this starts a completely new conversion. The current context information is ignored in this nested conversion.

Context

Ctx

Ctx accepts the following type parameters. When multiple parameters are given, all of them must match the context for a hook to fire.

  • Type: The target type, e.g., the declared type of a dataclass field when structuring it from a dict value. Literal, NewType, and unions are also allowed.
  • Root type: Under[T] means the type passed to structure or unstructure at the top of the current conversion. Lets a hook fire only when the conversion was entered at a specific type. See Root type example.
  • Owner type: Of[T] means the type of the owner (parent, container) of the current value.
  • Path expression: A string describing the position in the structured object graph. See Path expression.

An instance ctx of type Ctx has the following properties.

  • structured_type: The target type.
  • structured_key: Identifier of the current value within its owner. The concrete type depends on the owner:
    • dataclasses.Field instance for a dataclass field.
    • Field name (string) for a TypedDict field.
    • Integer index for a Sequence.
    • Key (any hashable type) for a dict value.
    • None for the root context or a set/frozenset element.
  • structured_path: The current path in the structured data graph.
  • unstructured_path: The current path in the unstructured data graph.
  • parent: The parent context. For example, if the current context is an element of a list, ctx.parent is the context of that list. None for the root context.

The two properties below are aliases that return structured_key as-is, with narrower declared return types to give type checkers and IDEs better hints. No conversion or validation occurs — calling them on the wrong kind of context returns whatever structured_key happens to hold.

  • field: typed as dataclasses.Field. Use on dataclass-field contexts.
  • index: typed as int. Use on sequence-item contexts.

The current value and the extra argument are accessed via helper functions.

  • get_data(ctx): The current value being processed. This duplicates the hook's data argument, but it is useful for navigating parent or sibling data via get_data(ctx.parent).
  • get_extra(ctx): The extra argument passed to structure or unstructure.
  • get_root(ctx): The root context.
  • get_parent(ctx): The same as ctx.parent.

Hook resolution

Matching with subtype relations

ctx: Ctx[T] matches only T exactly. Subtypes of T do not match. For example,

  • A structure hook with Ctx[date] does not fire when codify structures a field or element declared as datetime.
  • A structure hook with Ctx[int | str] does not fire when codify structures a field or element declared as int or str. It fires only when the field or element is declared as int | str.
  • A structure hook with Ctx[Literal[1, 2]] does not fire when codify structures a field or element declared as Literal[1] or Literal[2]. It fires only when the field or element is declared as Literal[1, 2] or Literal[1] | Literal[2].
  • A structure hook with Ctx[int] does not fire when codify structures a field or element declared as MyInt = NewType("MyInt", int). It fires only when the field or element is declared as MyInt.
  • A structure hook with Ctx[int] does not fire when codify structures a field or element declared as Literal[1].
  • A structure hook with Ctx[Foo[date]] does not fire when codify structures a field or element declared as Foo[datetime].
  • A structure hook with Ctx[Foo[Any]] or Ctx[Foo] fires when codify structures a field or element declared as Foo[str], Foo[Any] or Foo. Any used as a type parameter acts as a wildcard in exact match.

data: T matches T and any subtype of T. For example,

  • A structure hook with data: date accepts a datetime instance.

The register.ctx_subtypes decorator widens ctx matching to subtypes. For example,

  • A structure hook with Ctx[date] registered via register.ctx_subtypes fires when codify structures a field declared as datetime.
  • A structure hook with Ctx[int | str] registered via register.ctx_subtypes fires when codify structures a field declared as int or str.
  • A structure hook with Ctx[Literal[1, 2]] registered via register.ctx_subtypes fires when codify structures a field declared as Literal[1] or Literal[2].
  • A structure hook with Ctx[int] registered via register.ctx_subtypes fires when codify structures a field or element declared as MyInt = NewType("MyInt", int).
  • A structure hook with Ctx[int] registered via register.ctx_subtypes fires when codify structures a field declared as Literal[1] or Literal[2].
  • A structure hook with Ctx[Foo[date]] registered via register.ctx_subtypes fires when codify structures a field or element declared as Foo[datetime].
  • Exact match has higher priority. If a context matches both an exact hook and a subtype hook, the exact one wins.

Some notes on subtype relations

For both ctx and data, any subtype relation between generic types is assumed to be covariant. For example, list[datetime] is treated as a subtype of list[date], even though list is actually invariant.

Codify regards int as a subtype of float and float as a subtype of complex, even though issubclass returns False for these pairs.

Conflict resolution

What happens if the current context matches multiple hooks? In short, the most specific one wins. ctx takes priority and data is secondary.

  • There is no priority among the four "axes" (target type, root type, owner type, and path).
    • Ctx[int], Ctx[Under[Foo]], Ctx[Of[Foo]], and Ctx[".foo"] are not comparable.
  • Additionally specified axes are considered more specific.
    • Ctx[".foo", Of[Foo]] is more specific than Ctx[".foo"] or Ctx[Of[Foo]].
    • Ctx[Employee, Under[PublicReport]] is more specific than Ctx[Employee] or Ctx[Under[PublicReport]].
    • However, Ctx[int] and Ctx[".foo", Of[Foo]] are not comparable. If a context matches both, codify raises an exception during structure or unstructure.
  • Within each axis:
    • Types can be compared. This only matters when some hooks are registered with register.ctx_subtypes.
      • The notion of "more specific type" is intuitive but not simple to formalize. The examples below should be sufficient; see doc/multidispatch_spec.md for the precise spec.
      • int wins over float.
      • int wins over int | str.
      • int | str wins over int | str | date.
      • Literal[1, 2] wins over Literal[1, 2, 3].
      • list[int] wins over list[float].
      • list[int] wins over list or list[Any].
      • int and str cannot match the same context, so they are not comparable.
    • Of[S] is more specific than Of[T] when S is more specific than T. This only matters when some hooks are registered with register.ctx_subtypes.
    • Under[S] is more specific than Under[T] when S is more specific than T. Same caveat as Of.
    • A path S is more specific than another path T if T can be obtained from S by replacing some parts with the wildcard ?. A rootless path is treated as a rooted path whose leading prefix is a multi-level wildcard.
      • "$.foo[0]" is more specific than "$.foo[?]".
      • "$.foo" is more specific than ".foo".
      • "$.foo" and "$.foo.bar" cannot match the same context, so they are not comparable.
  • A more specific data type wins over a less specific one. data is consulted only when ctx types tie or are incomparable. For example, if Ctx[int | str] and Ctx[int | date] are registered with ctx_subtypes and both match a target type int, the type of data is consulted.
  • If the resolution rule fails to single out a hook, MultipleStructureHooks or MultipleUnstructureHooks is raised.

Note that hook registration order has no effect on resolution. If multiple hooks with the exact same signature are registered, the last one overrides the others.

See doc/multidispatch_spec.md for more details.

Restrictions on data type

Some types cannot be used as a data type hint in structure, unstructure, structure_hook, and unstructure_hook due to Python's limited runtime type information. Codify raises a TypeError when a hook with such a data hint is registered.

  • Python built-in generic containers in parameterized form are not allowed.
  • Generic classes with __slots__ are not allowed.
  • Generic dataclasses with slots=True or frozen=True are not allowed.
  • Types defined with NewType are not allowed.
  • TypedDict classes are not allowed.

For disallowed generic types, the bare type or parameterization with Any is allowed instead. For example, list or list[Any] are allowed, while list[int] is not.

These types can still appear as type parameters. For example, they can be a type parameter of Ctx or of a user-defined generic class used as the data hint. They are disallowed only as a direct top-level data hint.

The following explains why they are not allowed.

Restricted generic types

ctx: Ctx[list[int]] is fine. data: Foo[list[int]] is also fine. However, data: list[int] is not — this raises a TypeError at hook registration time.

Python's built-in containers do not carry runtime generic type information. Codify has no reliable way to determine whether [1, 2, 3] is a list[int] at runtime. Doing so would require inspecting every element, which is expensive. Even then, only compatibility can be established, not identity — [1, 2, 3] could be list[complex] or list[object]. A later value at the same context could be [1, 2.2, 3j, "a"]. This ambiguity would break hook resolution caching. For these reasons, codify rejects hooks with data: list[int].

Slotted generic classes and frozen dataclasses have the same issue. They also don't carry runtime generic type information.

More technically, codify relies on the __orig_class__ attribute to get the parameterization of an instance. These types don't carry __orig_class__.

NewTypes and TypedDicts

NewType has the same restriction. It is fine as a parameter of Ctx or as a type parameter of a user-defined generic type used as the data hint. But it cannot be used as a top-level data hint. An instance of a NewType carries no relevant runtime type information — for example, NewType("Int", int)(1) is just 1 of type int.

TypedDict has the same restriction. data: MyTypedDict is not allowed. Instances of a TypedDict are plain dict objects at runtime, not the TypedDict class. Use data: dict instead.

Note on user-defined generic classes

User-defined generic classes work anywhere. Note that codify does not infer runtime generic type parameters — you must explicitly parameterize a generic class. For example:

@dataclass
class Foo[T]:
    foo: T

@register
def structure_hook(ctx: Ctx[str], data: Foo[list[int]]) -> str:
    ...

# The hook fires.
structure(str, Foo[list[int]]([1, 2]))

# Invalid data type. Anyway the hook fires.
structure(str, Foo[list[int]](["a", "b"]))

# Caution: The hook does not fire. NoStructureHook error.
structure(str, Foo([1, 2]))

Configuration

codify.codify_config is a context manager to control the overall behavior of codify. Currently it provides only one option, dispatcher. See Scoped hooks.

Path expression

Note that in Ctx, the path expression refers to a location in structured data, not unstructured data.

Field

  • ".foo": field named foo of a dataclass or a TypedDict.
  • ".foo-bar": field named foo-bar of a TypedDict.
  • ".'foo bar'": field named foo bar of a TypedDict.
  • ".?": any field of a dataclass or a TypedDict.

A dataclass field name must be a valid Python identifier, while a TypedDict field name can be any string. Field expressions that are not plain identifiers or kebab-case names must be enclosed in quotation marks (" or ').

Item

Here an item is an element of a Sequence accessed by integer index, or a value in a dict.

  • "[0]", "[1]", ...: the element at index 0, 1, ... of a list, tuple, or other Sequence, or the value at integer key 0, 1, ... in a dict.
  • "['foo']": the value at string key "foo" in a dict.
  • "[Foo.A]": the value at Enum key Foo.A in a dict.
  • "[?]": any item.

Note that the Enum key Foo.A matches any Enum named Foo regardless of which module defines it.

Dict key

These match the keys themselves, not the values they index.

  • "[~0]", "[~1]", ...: integer dict key.
  • "[~'foo']": string dict key "foo".
  • "[~Foo.A]": Enum dict key Foo.A.
  • "[~?]": any dict key.

"[~0]", "[~1]", ... do not represent indices of a sequential container. The note on Enum in the previous section also applies here.

Note that during structure, when codify converts an unstructured dict key into the structured key, the structured key does not exist yet. As a result, only "[~?]" can match — a specific dict-key path cannot. Thus codify raises a TypeError when a structure_hook with a specific dict-key path is registered. By contrast, when structure converts a dict value, the structured key has already been produced (codify converts the key before the value), so a specific item path like "['foo']" can match. During unstructure, the structured dict key always exists, so a specific dict-key path can match.

Path

The above expressions can be composed into a path starting with the root marker "$". For example, "$.foo[0].?[?]" means

  • any item of
    • any field of
      • the first item of
        • the foo field of
          • the root.

The wildcard "?" does not span multiple levels.

Rootless expressions (those without a leading "$") are always single-segment — a bare field expression like ".foo" or a bare item expression like "[?]". Multi-segment paths must be rooted, i.e., start with "$". For example, "$.foo[?]" is valid, but ".foo[?]" is not.

Anything

  • "?" or Any: anything.

Technically "?" is a location constraint and Any is a type constraint, but either alone is equivalent to "anything" — Ctx["?"] and Ctx[Any] produce the same context type.

Error

All exceptions raised by codify inherit from codify.CodifyError. Catch that to handle any codify-raised failure uniformly.

CodifyError
├── NoStructureHook
├── NoUnstructureHook
├── MultipleStructureHooks
├── MultipleUnstructureHooks
├── AmbiguousUnion
├── ReentranceError
└── ValidationError
    ├── MissingFields
    └── ExtraFields
  • NoStructureHook / NoUnstructureHook — no hook (user-registered or default) matched the current target type and data type.
  • MultipleStructureHooks / MultipleUnstructureHooks — more than one hook matched and the conflict resolution rules couldn't single one out. See Conflict resolution.
  • ValidationError — raised by hooks (including built-ins) to signal that the data is invalid for the target type. Hook authors typically raise this directly: raise ValidationError(ctx, data, "...").
  • MissingFields / ExtraFields — specializations of ValidationError raised by the default dataclass / TypedDict handlers when required fields are absent or unexpected keys are present.
  • AmbiguousUnion — union dispatch could not pick a single candidate (either probing matched several, or the chosen members are indistinguishable).
  • ReentranceError — raised when structure or unstructure is called while another conversion is already running on the same dispatcher.

Every codify error exposes these attributes:

  • ctx — the Ctx at which the error was raised. ctx.structured_path is included in the default message; use ctx.unstructured_path, ctx.structured_type, ctx.parent, get_extra(ctx), etc. for richer inspection.
  • data — the data value that triggered the error.

Specific errors carry extra information:

  • candidates on MultipleStructureHooks and MultipleUnstructureHooks — the list of competing Methods; the default message includes their signatures and source locations.
  • missing on MissingFields — list of required field names that were absent.
  • extra on ExtraFields — list of unexpected key names that were present.

Multidispatch

This section explains the multidispatch system underlying codify's hook system. You can skip it if you are not interested in general multidispatch — you do not need to know about it to use codify. That said, the system can also be used for general-purpose dispatching outside of codify's hooks.

If you are unfamiliar with the term "multidispatch" but have experience with languages that support "overloading", think of it as overloading dispatched by argument types at runtime rather than at compile time.

from codify import register

@register
def foo(a: int) -> str:
    return "int"

@register
def foo(a: float) -> str:
    return "float"

@register
def foo(a: int, b: float) -> str:
    return "int, float"

foo(1)  # 'int'
foo(1.0)  # 'float'
foo(1, 1.0)  # 'int, float'

a: float = 1
foo(a)  # 'int', by the runtime type `int` rather than the static type `float`.

To create a new dispatcher without codify's default handlers, use the @codify.multidispatch.dispatch decorator, or create a new instance of codify.multidispatch.Dispatch and use it as a decorator.

There are other Python multidispatch libraries like plum or multimethod. Codify's system differs in a few ways.

  • The dispatch resolution is lexicographic. Earlier (left) arguments are filtered first; ties at earlier positions can be broken by later ones.
  • Arguments with default values are not allowed. Keyword-only arguments are allowed but are not used for dispatch resolution.
  • The variance mode (covariance or invariance) can be declared per argument position. The default is invariant. Covariant positions are specified with the @dispatch.covariant(*positions) decorator, where dispatch is any instance of codify.multidispatch.Dispatch.
    • Any acts as a wildcard on the right-hand side of invariant subtype relations.
    • Never acts as a wildcard on the left-hand side of invariant subtype relations.
    • @codify.register.ctx_subtypes marks the specified parameterization of CtxImpl[...] (produced by Ctx[...]) as covariant. The default invariant mode of Ctx behaves like exact matching on Ctx's type parameters.
  • Virtual base classes: DataClassBase, LiteralBase, NewTypeBase, TypedDictBase, and UnionBase.
  • No runtime generic type inference. Only explicit parameterization exposed via __orig_class__ is recognized.

These design decisions are driven by codify's requirements. Some aspects of Python's type system are deliberately respected while others are not, in order to keep the dispatch system's API and implementation practical.

See doc/multidispatch_spec.md for more details.

Caveats

Internal caching

Codify creates an internal converter for each root type when structure or unstructure is called with the type as the target or declared type for the first time. The first call is slow. On subsequent calls for the same root type, the cached converter is reused and the call is much faster. Any hook registration invalidates the converter cache. When a dispatcher is copied, the converter cache is not copied. The copied dispatcher creates its own fresh converter cache.

Thread safety

structure and unstructure are not thread-safe. For thread safety, use a separate dispatcher per thread. See Scoped hooks for how to create an independent copy of the underlying dispatcher.

NewType and TypedDict as data type

As explained in Restrictions on data type, NewTypes and TypedDicts are not allowed as the data type in a hook. In unstructure hooks it is easy to accidentally use them there, which causes a TypeError at hook registration.

User-defined generic classes

Codify does not infer generic type parameters. You must explicitly parameterize a generic class. See Note on user-defined generic classes.

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

codify_conv-1.0.0.tar.gz (88.0 kB view details)

Uploaded Source

Built Distribution

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

codify_conv-1.0.0-py3-none-any.whl (57.6 kB view details)

Uploaded Python 3

File details

Details for the file codify_conv-1.0.0.tar.gz.

File metadata

  • Download URL: codify_conv-1.0.0.tar.gz
  • Upload date:
  • Size: 88.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for codify_conv-1.0.0.tar.gz
Algorithm Hash digest
SHA256 6586fe5f5dd082c3fd7dd9595585b152b85106778452b870649628593ce6cb27
MD5 1136da2dd0d28fc908a0a55bfe308323
BLAKE2b-256 f301213103420f8e7fea78d99cc82aa73bf8e04bbf95690a2f17eb6020ff6c2c

See more details on using hashes here.

Provenance

The following attestation bundles were made for codify_conv-1.0.0.tar.gz:

Publisher: release.yml on cpcprmrm/codify

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file codify_conv-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: codify_conv-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 57.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for codify_conv-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d0f3f7022bb439559aff71f09cea7930c0ed565bc434e5c794e33a2b0ac051de
MD5 d90f2399895deddc93cd94781cc8ed5d
BLAKE2b-256 80db40cc10e33586c017ea5b4e18117eb47483e530be77868b0b8bb39f0e6cf0

See more details on using hashes here.

Provenance

The following attestation bundles were made for codify_conv-1.0.0-py3-none-any.whl:

Publisher: release.yml on cpcprmrm/codify

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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