Skip to main content

dataclassish

Tools from dataclasses, extended to all of Python

PyPI: dataclassish PyPI versions: dataclassish dataclassish license

CI status codecov ruff ruff pre-commit


Python's dataclasses provides tools for working with objects, but only compatible @dataclass objects. 😢
This repository is a superset of those tools and extends them to work on ANY Python object you want! 🎉
You can easily register in object-specific methods and use a unified interface for object manipulation. 🕶️

For example,

from dataclassish import replace  # New object, replacing select fields

d1 = {"a": 1, "b": 2.0, "c": "3"}
d2 = replace(d1, c=3 + 0j)
print(d2)
# {'a': 1, 'b': 2.0, 'c': (3+0j)}

Installation

PyPI platforms PyPI version

pip install dataclassish

Documentation

Getting Started

Replacing a @dataclass

In this example we'll show how dataclassish works exactly the same as dataclasses when working with a @dataclass object.

>>> from dataclassish import replace
>>> from dataclasses import dataclass

>>> @dataclass(frozen=True)
... class Point:
...     x: float | int
...     y: float | int


>>> p = Point(1.0, 2.0)
>>> p
Point(x=1.0, y=2.0)

>>> p2 = replace(p, x=3.0)
>>> p2
Point(x=3.0, y=2.0)

Replacing a dict

Now we'll work with a dict object. Note that dataclasses does not work with dict objects, but with dataclassish it's easy!

>>> from dataclassish import replace

>>> p = {"x": 1, "y": 2.0}
>>> p
{'x': 1, 'y': 2.0}

>>> p2 = replace(p, x=3.0)
>>> p2
{'x': 3.0, 'y': 2.0}

# If we try to `replace` a value that isn't in the dict, we'll get an error
>>> try:
...     replace(p, z=None)
... except ValueError as e:
...     print(e)
invalid keys {'z'}.

Replacing via the __replace__ Method

In Python 3.13+ objects can implement the __replace__ method to define how copy.replace should operate on them. This was directly inspired by dataclass.replace, and is a nice generalization to more general Python objects. dataclassish too supports this method.

>>> class HasReplace:
...     def __init__(self, a, b):
...         self.a = a
...         self.b = b
...     def __repr__(self) -> str:
...         return f"HasReplace(a={self.a},b={self.b})"
...     def __replace__(self, **changes):
...         return type(self)(**(self.__dict__ | changes))

>>> obj = HasReplace(1, 2)
>>> obj
HasReplace(a=1,b=2)

>>> obj2 = replace(obj, b=3)
>>> obj2
HasReplace(a=1,b=3)

Replacing a Custom Type

Let's say there's a custom object that we want to use replace on, but which doesn't have a __replace__ method (or which we want more control over using a second argument, discussed later). Registering in a custom type is very easy! Let's make a custom object and define how replace will operate on it.

>>> from typing import Any
>>> from plum import dispatch

>>> class MyClass:
...     def __init__(self, a, b, c):
...         self.a = a
...         self.b = b
...         self.c = c
...     def __repr__(self) -> str:
...         return f"MyClass(a={self.a},b={self.b},c={self.c})"


>>> @dispatch
... def replace(obj: MyClass, **changes: Any) -> MyClass:
...     current_args = {k: getattr(obj, k) for k in "abc"}
...     updated_args = current_args | changes
...     return MyClass(**updated_args)


>>> obj = MyClass(1, 2, 3)
>>> obj
MyClass(a=1,b=2,c=3)

>>> obj2 = replace(obj, c=4.0)
>>> obj2
MyClass(a=1,b=2,c=4.0)

Nested Replacement

replace can also accept a second positional argument which is a dictionary specifying a nested replacement. For example consider the following dict of Point objects:

>>> p = {"a": Point(1, 2), "b": Point(3, 4), "c": Point(5, 6)}

With replace the nested structure can be updated via:

>>> replace(p, {"a": {"x": 1.5}, "b": {"y": 4.5}, "c": {"x": 5.5}})
{'a': Point(x=1.5, y=2), 'b': Point(x=3, y=4.5), 'c': Point(x=5.5, y=6)}

In contrast in pure Python this would be very challenging. Expand the example below to see how this might be done.

Expand for detailed example

This is a bad approach, updating the frozen dataclasses in place:

>>> from copy import deepcopy

>>> newp = deepcopy(p)
>>> object.__setattr__(newp["a"], "x", 1.5)
>>> object.__setattr__(newp["b"], "y", 4.5)
>>> object.__setattr__(newp["c"], "x", 5.5)

A better way might be to create an entirely new object!

>>> newp = {"a": Point(1.5, p["a"].y),
...         "b": Point(p["b"].x, 4.5),
...         "c": Point(5.5, p["c"].y)}

This isn't so good either.

dataclassish.replace is a one-liner that can work on any object (if it has a registered means to do so), regardless of mutability or nesting. Consider this fully immutable structure:

>>> @dataclass(frozen=True)
... class Object:
...     x: float | dict
...     y: float


>>> @dataclass(frozen=True)
... class Collection:
...     a: Object
...     b: Object


>>> p = Collection(Object(1.0, 2.0), Object(3.0, 4.0))
>>> p
Collection(a=Object(x=1.0, y=2.0), b=Object(x=3.0, y=4.0))

>>> replace(p, {"a": {"x": 5.0}, "b": {"y": 6.0}})
Collection(a=Object(x=5.0, y=2.0), b=Object(x=3.0, y=6.0))

With replace this remains a one-liner. Replace pieces of any structure, regardless of nesting.

To disambiguate dictionary fields from nested structures, use the F marker.

>>> from dataclassish import F

>>> replace(p, {"a": {"x": F({"thing": 5.0})}})
Collection(a=Object(x={'thing': 5.0}, y=2.0),
           b=Object(x=3.0, y=4.0))

dataclass tools

dataclasses has a number of utility functions beyond replace: fields, asdict, and astuple. dataclassish supports of all these functions.

>>> from dataclassish import fields, asdict, astuple

>>> p = Point(1.0, 2.0)

>>> fields(p)
(Field(name='x',...), Field(name='y',...))

>>> asdict(p)
{'x': 1.0, 'y': 2.0}

>>> astuple(p)
(1.0, 2.0)

dataclassish extends these functions to dict's:

>>> p = {"x": 1, "y": 2.0}

>>> fields(p)
(Field(name='x',...), Field(name='y',...))

>>> asdict(p)
{'x': 1, 'y': 2.0}

>>> astuple(p)
(1, 2.0)

Support for custom objects can be implemented similarly to replace.

More tools

In addition to the dataclasses tools, dataclassish provides a few more utilities.

  • get_field returns the field of an object by name.
  • field_keys returns the names of an object's fields.
  • field_values returns the values of an object's fields.
  • field_items returns the names and values of an object's fields.
>>> from dataclassish import get_field, field_keys, field_values, field_items

>>> p = Point(1.0, 2.0)

>>> get_field(p, "x")
1.0

>>> field_keys(p)
('x', 'y')

>>> field_values(p)
(1.0, 2.0)

>>> field_items(p)
(('x', 1.0), ('y', 2.0))

These functions work on any object that has been registered in, not just @dataclass objects.

>>> p = {"x": 1, "y": 2.0}

>>> get_field(p, "x")
1

>>> field_keys(p)
dict_keys(['x', 'y'])

>>> field_values(p)
dict_values([1, 2.0])

>>> field_items(p)
dict_items([('x', 1), ('y', 2.0)])

Converters

While dataclasses.field itself does not allow for converters (See PEP 712) many dataclasses-like libraries do. A very short, very non-exhaustive list includes: attrs and equinox. The module dataclassish.converters provides a few useful converter functions. If you need more, check out attrs!

>>> from attrs import define, field
>>> from dataclassish.converters import Optional, Unless


>>> @define
... class Class1:
...     attr: int | None = field(default=None, converter=Optional(int))
...     """attr is converted to an int or kept as None."""


>>> obj = Class1()
>>> print(obj.attr)
None

>>> obj = Class1(attr=1.0)
>>> obj.attr
1

>>> @define
... class Class2:
...     attr: float | int = field(converter=Unless(int, converter=float))
...     """attr is converted to a float, unless it's an int."""

>>> obj = Class2(1)
>>> obj.attr
1

>>> obj = Class2("1")
>>> obj.attr
1.0

This library also provide a lightweight dataclass-like decorator and field function that supports these converters and converters in general.

>>> from dataclassish.converters import dataclass, field

>>> @dataclass
... class MyClass:
...     a: int | None = field(converter=Optional(int))
...     b: str = field(converter=str.upper)

>>> obj = MyClass(a="1", b="hello")
>>> obj
MyClass(a=1, b='HELLO')

>>> obj = MyClass(a=None, b="there")
>>> obj
MyClass(a=None, b='THERE')

Flags

dataclassish provides flags for customizing the behavior of functions. For example, the coordinax package, which depends on dataclassish, uses a flag AttrFilter to filter out fields from consideration by the functions in dataclassish.

dataclassish provides a few built-in flags and flag-related utilities.

>>> from dataclassish import flags
>>> flags.__all__
('FlagConstructionError', 'AbstractFlag', 'NoFlag', 'FilterRepr')

Where AbstractFlag is the base class for flags, NoFlag is a flag that does nothing, and FilterRepr will filter out any fields with repr=True. FlagConstructionError is an error that is raised when a flag is constructed incorrectly.

As a quick example, we'll show how to use NoFlag.

>>> from dataclassish import field_keys
>>> tuple(field_keys(flags.NoFlag, p))
('x', 'y')

As another example, we'll show how to use FilterRepr.

>>> from dataclasses import field
>>> @dataclass
... class Point:
...     x: float
...     y: float = field(repr=False)
>>> obj = Point(1.0, 2.0)

>>> field_keys(flags.FilterRepr, obj)
('x',)

Citation

DOI

If you enjoyed using this library and would like to cite the software you use then click the link above.

Development

Actions Status codecov SPEC 0 — Minimum Supported Dependencies pre-commit ruff

We welcome contributions!

Metadata

Release files for dataclassish 0.9.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dataclassish 0.9.1
File Size Uploaded
dataclassish-0.9.1.tar.gz 73.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dataclassish 0.9.1
File Interpreter ABI Platform
dataclassish-0.9.1-py3-none-any.whl Python 3 none any Details

Total release size: 98.0 kB

Release files / dataclassish-0.9.1.tar.gz

Download URL dataclassish-0.9.1.tar.gz
Size 73.4 kB
Tags Source
SHA-256 checksum
How to use checksums
145e3520a59e20afd87bf42c2784ffaabf6efcad4ccee121f0f3c01a9a80ebc5
BLAKE2b-256 checksum
How to use checksums
c3f54fcfefb2045292a096213f47a1212417fefc002a27a07d3991959c1d821b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.

Transparency log

Release files / dataclassish-0.9.1-py3-none-any.whl

Download URL dataclassish-0.9.1-py3-none-any.whl
Size 24.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8a63a0f12dc0937ddf8d0c8446b99ba8649eaa096158f27e200eafcafc6e627b
BLAKE2b-256 checksum
How to use checksums
e406088212350c638eaa40ca26e0c4a610539b0158b679988da3695817da2414
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.

Transparency log

Release history Release notifications | RSS feed

0.9.2

2 release files

This release

0.9.1 This release

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page