Skip to content

Optionally exclude defaults and None #639

@redruin1

Description

@redruin1

I have a seemingly simple problem: I have an attrs class that I want to serialize to a Python dictionary, but I want to be able to prune outputs if they're None or equal to their default values, e.g:

@attrs.define
class Example:
    a: int
    b: int | None
    c: int = 123
    
    def to_dict(self, exclude_none: bool, exclude_defaults: bool) -> dict:
        ... # TODO

e = Example(a=100, b=None)
e.to_dict(exclude_none=False, exclude_defaults=False) # {'a': 100, 'b': None, 'c': 123}
e.to_dict(exclude_none=True, exclude_defaults=False) # {'a': 100, 'c': 123}
e.to_dict(exclude_none=False, exclude_defaults=True) # {'a': 100, 'b': None}
e.to_dict(exclude_none=True, exclude_defaults=True) # {'a': 100}

omit_if_default seems to do half of what I want already, and I was hopeful that I could do something like:

converter = cattrs.Converter(omit_if_default=True)

class Example:
    ...
    def to_dict(self, exclude_none: bool, exclude_defaults: bool) -> dict:
        converter.omit_if_default = exclude_defaults
        return converter.unstructure(self)

But after taking a closer look, it seems the unstructure function is only generated once with the current value of omit_if_default, and then cached for reuse (for performance reasons I imagine). Does this imply that the "correct" way to do this is to create 4 separate hooks for every possible class I want to unstructure, and then select between them based on the passed in arguments?

If push comes to shove I can just generate the maximum dict and then manually prune myself:

converter = cattrs.Converter(omit_if_default=False)

class Example:
    ...
    def to_dict(self, exclude_none: bool, exclude_defaults: bool) -> dict:
        defaults = {field.name: field.default for field in attrs.fields(type(self))}
        return {
            k: v for k, v in converter.unstructure(self, type(self)).items()
            if not ((v is None and exclude_none) or (getattr(self, k) == defaults[k] and exclude_defaults))
        }

But this doesn't support removing None/defaults recursively through the structure, and it feels like I'm circumventing cattrs instead of properly using it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions