Search code examples
pythonpython-dataclasses

Exclude some attributes from __str__ representation of a dataclass


We have this class:

from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict

@dataclass
class BoardStaff:
    date: str = datetime.now()
    fullname: str
    address: str

    ## attributes to be excluded in __str__:
    degree: str
    rank: int = 10
    badges: bool = False
    cases_dict: Dict[str, str] = field(default_factory=dict)
    cases_list: List[str] = field(default_factory=list)

Emp = BoardStaff('Jack London', address='Unknown', degree='MA')

As BoardStaff is a dataclass, one can easily do print(Emp) to receive:
BoardStaff(fullname='Jack London', address='Unknown', degree='MA', rank=10, badges=False, cases={}, date=datetime.datetime(2021, 8, 10, 11, 36, 50, 693428)).

However, I want some attributes (i.e. the last 5 ones) to be excluded from the representation, so I had to define __str__ method and manually exclude some attributes like so:

    def __str__(self):
        str_info = {
            k: v
            for k, v in self.__dict__.items()
            if k not in ['degree', 'rank', 'other'] and v
        }
        return str(str_info)

But is there a better way to do the exclusion, like using some parameters when defining the attributes?


Solution

  • Obvious solution

    Simply define your attributes as fields with the argument repr=False:

    from dataclasses import dataclass, field
    from datetime import datetime
    from typing import List, Dict
    
    @dataclass
    class BoardStaff:
        date: str = datetime.now()
        fullname: str
        address: str
    
        ## attributes to be excluded in __str__:
        degree: str = field(repr=False)
        rank: int = field(default=10, repr=False)
        badges: bool = field(default=False, repr=False)
        cases_dict: Dict[str, str] = field(default_factory=dict, repr=False)
        cases_list: List[str] = field(default_factory=list, repr=False)
    
    Emp = BoardStaff('Jack London', address='Unknown', degree='MA')
    

    This works nicely alongside marking attributes as "private" by giving them names starting with leading underscores, as others have suggested in the comments.

    More advanced solutions

    If you're looking for a more general solution that doesn't involve defining so many fields with repr=False, you could do something like this. It's pretty similar to the solution you thought up yourself, but it creates a __repr__ that's more similar to the usual dataclass __repr__:

    from dataclasses import dataclass, field
    from datetime import datetime
    from typing import List, Dict
        
    @dataclass
    class BoardStaff:
        fullname: str
        address: str
        degree: str
        date: str = datetime.now()
        rank: int = 10
        badges: bool = False
        cases_dict: Dict[str, str] = field(default_factory=dict)
        cases_list: List[str] = field(default_factory=list)
    
        def __repr__(self):
            dict_repr = ', '.join(
                f'{k}={v!r}'
                for k, v in filter(
                    lambda item: item[0] in {'fullname', 'address', 'date'},
                    self.__dict__.items()
                )
            )
    
            return f'{self.__class__.__name__}({dict_repr})'
    
    Emp = BoardStaff('Jack London', address='Unknown', degree='MA')
    print(Emp)
    

    (N.B. I had to reorder your fields slightly, as having default-argument parameters before parameters with no default will raise an error.)

    If you don't want to hardcode your __repr__ fields into your __repr__ methods, you could mark your non-__repr__ fields as private attributes, as was suggested in the comments by @DarkKnight, and use this as a signal for your __repr__ method:

    from dataclasses import dataclass, field
    from datetime import datetime
    from typing import List, Dict
        
    @dataclass
    class BoardStaff:
        fullname: str
        address: str
        _degree: str
        date: str = datetime.now()
        _rank: int = 10
        _badges: bool = False
        _cases_dict: Dict[str, str] = field(default_factory=dict)
        _cases_list: List[str] = field(default_factory=list)
    
        def __repr__(self):
            dict_repr = ', '.join(
                f'{k}={v!r}'
                for k, v in filter(
                    lambda item: not item[0].startswith('_'),
                    self.__dict__.items()
                )
            )
    
            return f'{self.__class__.__name__}({dict_repr})'
    
    Emp = BoardStaff('Jack London', address='Unknown', _degree='MA')
    print(Emp)
    

    You could even potentially write your own decorator that would generate custom __repr__ methods for you on a class-by-class basis. E.g., this decorator will generate __repr__ methods that will only include the arguments you pass to the decorator:

    from dataclasses import dataclass, field
    from datetime import datetime
    from typing import List, Dict
    from functools import partial
    
    def dataclass_with_repr_fields(
        keys, init=True, eq=True, order=False, 
        unsafe_hash=False, frozen=False, cls=None
    ):
        if cls is None:
            return partial(
                dataclass_with_repr_fields, keys, init=init, 
                eq=eq, order=order,  unsafe_hash=unsafe_hash,
                frozen=frozen)
    
        cls = dataclass(
            cls, init=init, repr=False, eq=eq, order=order, 
            unsafe_hash=unsafe_hash, frozen=frozen
        )
    
        def __repr__(self):
            dict_repr = ', '.join(
                f'{k}={v!r}'
                for k, v in filter(
                    lambda item: item[0] in keys,
                    self.__dict__.items()
                )
            )
    
            return f'{self.__class__.__name__}({dict_repr})'
    
        cls.__repr__ = __repr__
        return cls
    
    
    @dataclass_with_repr_fields({'fullname', 'address', 'date'})
    class BoardStaff:
        fullname: str
        address: str
        degree: str
        date: str = datetime.now()
        rank: int = 10
        badges: bool = False
        cases_dict: Dict[str, str] = field(default_factory=dict)
        cases_list: List[str] = field(default_factory=list)
        
    
    @dataclass_with_repr_fields({'name', 'surname'})
    class Manager:
        name: str
        surname: str
        salary: int
        private_medical_details: str
    
    Emp = BoardStaff('Jack London', address='Unknown', degree='MA')
    print(Emp)
    manager = Manager('John', 'Smith', 600000, 'badly asthmatic')
    print(manager)