A lightweight package for converting dataclass hierarchies to and from dictionaries.
Project description
DataclassIO
DataclassIO is a package for fast roundtrip conversion between python literals and dataclass hierarchies.
This project is in its early stages. If you need maturity and lots of bells of whistles, consider another library :)
Supported Types and Constructs
Currently, the following types are supported:
- Optional expressions:
Optional[T],T | None, orUnion[T, None] - Embedded dataclasses:
DataclassType(exports asdict)- Unions of multiple dataclasses are supported via a discriminator.
- Lists:
list[T] - Dicts:
dict[TK, TV] - Enums:
Enum(exports asstr) - Datetimes:
datetime(imports/exports to ISO-8601 string) - fundamental types: e.g.,
int,float,str,bool
Where the T, TK, and TV type variables may be any other type listed in the table.
For instance, dict[str, list[DataclassType | None]] is supported.
The library also supports common dataclass features:
- Default values and default factories:
field(default=..., default_factory=...) - Init-only and
init=Falsefields- Note that
init=Falsefields are still exported.
- Note that
kw_onlyfields and dataclasses.- Class variables.
The library also supports a handful of hooks that can be used to customize the serialization or deserialization process, or run other side effects during to_dict and from_dict calls.
Example
from dataclasses import dataclass, field
from dataclassio import IOMixin, EFS
@dataclass
class Address:
city: str
zip_code: tp.Optional[str] = None
@dataclass
class User(IOMixin):
id: int
name: str
is_admin: bool = False
address: tp.Optional[Address] = None # <-- Address does not derive from IOMixin!
named_addresses: dict[str, Address] = field(default_factory=dict)
## Dictionary -> Dataclass
data = {
"id": 1,
"name": "Alice",
"named_addresses": {"home": {"city": "Anytown"}, "work": {"city": "Coolsville"}},
}
actual = User.from_dict(data)
assert isinstance(actual.named_addresses, dict)
assert isinstance(actual.named_addresses["home"], Address)
assert actual.named_addresses["work"].city == "Coolsville"
## Dataclass -> Dictionary (note that the default-valued-fields are excluded)
dikt = actual.to_dict(skip_defaults=True)
assert dikt == data
## Extra Fields are captured:
address_data = {
"city": "Anytown",
"bonus": "extra",
}
address_obj = Address.from_dict(address_data, extra_field_strategy=EFS.CAPTURE)
assert address_obj.extra_fields == {"bonus": "extra"}
assert address_obj.to_dict() == address_data # extra fields survive round trip.
Configuration
A handful of configuration options are supported to customize serialization and deserialization. These can be provided either at time of a to_dict or from_dict call ("call-level config") or can be applied to individual dataclass fields statically ("field-level config"). Not all options can be provided in both places. The following options are supported:
disable_hooks: A flag to skipto_dictandfrom_dicthooks, even if they are defined on a type.discriminator: The name of a field to use when discrimating between unions of dataclass types.extra_field_strategy: The preferred method for handling unexpected fields during deserialization (from_dict). By default they are ignored. Alternatively, an exception can be raised (STRICT-mode) or they can be captured into a private attribute of the object (CAPTURE-mode). InCAPTURE-mode, the unexpected fields are yielded in theto_dictcall, supporting full round-tripping.include_src_in_docstring: A flag to includeto_dictandfrom_dictmethod source code in their corresponding docstrings. This is useful since their source code is dynamically generated.skip: Field-level flag to omit a field fromto_dict.skip_extras: Call-level flag to omit any cpatured extra fields fromto_dict. If you know that your instances do not have extras, or don't care to preserve this, this can be safely toggled toTrue.skip_if_default/skip_defaults: Flags to omit fields during theto_dictcall if they have the default value.skip_if_defaultis field-level only, and has the highest precedence.skip_defaultscan be applied at the call- and field-level. At the field-level,skip_defaultshas a slightly different meaning fromskip_if_default:x: T = field(..., FieldOptions(skip_if_default=True)): Suppress serialization of the fieldxif its value equals the default.x: T = field(..., FieldOptions(skip_defaults=True)): Suppress serialization ofT's fields if they have a default value.
Propagation and Precedence
When a configuration option is specified at both the call- and field-level, the field-level option is preferred.
In addition to precedence differences, options propagate differently depending on where they are defined. Call-level config options always propagate through fields which are themselves dataclass-typed. Field-level config options do not.
Field-level options can be divided into "local" and "deep once" propagation categories. "Local" options (e.g., discriminator) affect the code-generation for their contained dataclass but not the nested dataclass. Meanwhile, "deep once" options (e.g., extra_field_strategy) are the inverse - they do not affect the code-generation for their contained dataclass but do for the affected field. Most of the time, this distinction is the natural interpretation and does not need to be top of mind.
Benchmarks
Dict -> Dataclass
We compare the deserialization (i.e., from_dict) methods for dataclassio and similar python libraries using the COCO2017 validation dataset (~20MB; 5000 images; 36781 annotations), which is a large but shallow JSON hierarchy. For benchmarking, the JSON file is first loaded and converted to python builtins. This dataset has no extra fields, so the equivalent of extra_field_strategy=EFS.IGNORE is utilized for all libraries.
We obtain the following times using pybench run benchmarks/compare_with_other_libraries.py --profile thorough:
| benchmark | time (avg) | p99 | vs base |
|---|---|---|---|
| dataclassio ★ | 48.39 ms | 50.26 ms | baseline |
| pydantic v2 | 85.82 ms | 93.31 ms | 1.77× slower |
| msgspec_into_dataclass | 39.22 ms | 42.99 ms | 1.23× faster |
| msgspec_into_native | 19.97 ms | 20.80 ms | 2.42× faster |
| mashumaro | 62.42 ms | 63.09 ms | 1.29× slower |
| dataclass-wizard | 63.40 ms | 65.95 ms | 1.31× slower |
Only msgspec is significantly faster dataclassio, but it does not support capturing extra fields. As a key distinction, dataclassio does not try to validate or coerce types, which may explain some or all of the performance advantages relative to other libraries, which may be coercing by default. Other libraries are also more configurable, may support more types, and so forth. However, if all you need is fast dict -> dataclass conversion that supports capturing unexpected fields, then dataclassio may be a good fit.
Dataclass -> Dict
Using the same large COCO2017 dataset, we also evaluate conversion back to python literals (e.g., to_dict) using the default settings for all libraries.
We obtain the following times using pybench run benchmarks/compare_to_dict.py --profile smoke:
| benchmark | time (avg) | p99 | vs base |
|---|---|---|---|
| dataclassio ★ | 28.25 ms | 28.30 ms | baseline |
| native | 376.97 ms | 377.70 ms | 13.34× slower |
| pydantic | 130.21 ms | 130.70 ms | 4.61× slower |
| msgspec_from_dataclass | 92.68 ms | 93.50 ms | 3.28× slower |
| msgspec_from_struct | 86.26 ms | 87.91 ms | 3.05× slower |
| mashumaro | 29.74 ms | 30.14 ms | 1.05× slower |
| dataclass-wizard | 33.81 ms | 35.12 ms | 1.20× slower |
In this test, dataclassio was the fastest library available, though mashumaro is probably equivalent.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hill_dataclassio-0.3.0.tar.gz.
File metadata
- Download URL: hill_dataclassio-0.3.0.tar.gz
- Upload date:
- Size: 22.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a41dae5eb5cb52552744e285e704ca81ce2cd4775c414c2dd7cd90ae97aeaa8
|
|
| MD5 |
d3f5ccea5d0a34c2008ca2642074e064
|
|
| BLAKE2b-256 |
fde4162175c2ddf89007f3ac9559866bfe2a8e1d9a2292dd8457252d1075f95d
|
Provenance
The following attestation bundles were made for hill_dataclassio-0.3.0.tar.gz:
Publisher:
python-publish.yml on charlesjhill/dataclassio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hill_dataclassio-0.3.0.tar.gz -
Subject digest:
5a41dae5eb5cb52552744e285e704ca81ce2cd4775c414c2dd7cd90ae97aeaa8 - Sigstore transparency entry: 1649936229
- Sigstore integration time:
-
Permalink:
charlesjhill/dataclassio@d64e456459cadc0e1bd6d56f9df380e1541ed6de -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/charlesjhill
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@d64e456459cadc0e1bd6d56f9df380e1541ed6de -
Trigger Event:
push
-
Statement type:
File details
Details for the file hill_dataclassio-0.3.0-py3-none-any.whl.
File metadata
- Download URL: hill_dataclassio-0.3.0-py3-none-any.whl
- Upload date:
- Size: 29.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95a4676070642955f0be60fcc23e450c1afb290845d86d37a799fb808593be9a
|
|
| MD5 |
501f5f77b8d7ff47fe167991a6c4105e
|
|
| BLAKE2b-256 |
d983dd5e39410619d4feef893394f4d31cace65707682d4f93f04d98f6617e56
|
Provenance
The following attestation bundles were made for hill_dataclassio-0.3.0-py3-none-any.whl:
Publisher:
python-publish.yml on charlesjhill/dataclassio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hill_dataclassio-0.3.0-py3-none-any.whl -
Subject digest:
95a4676070642955f0be60fcc23e450c1afb290845d86d37a799fb808593be9a - Sigstore transparency entry: 1649936291
- Sigstore integration time:
-
Permalink:
charlesjhill/dataclassio@d64e456459cadc0e1bd6d56f9df380e1541ed6de -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/charlesjhill
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@d64e456459cadc0e1bd6d56f9df380e1541ed6de -
Trigger Event:
push
-
Statement type: