Skip to content

c108.dictify

Comprehensive object-to-dictionary conversion toolkit.

Provides configurable, recursive transformation of arbitrary Python objects into human-readable or JSON-serializable dictionaries. Features include dynamic depth control, type-specific handlers, customizable attribute inclusion, collection trimming with metadata injection, and optional class name annotation for logging, serialization or debugging.

ClassNameOptions dataclass

Configuration for class name injection in objects converted to mappings.

Controls how and where class names appear in the output, useful for object reconstruction, debugging, and type tracking.

Class name injection can be used only for an object represented as mapping when processed with dictify(), this includes but is not limited to objects implementing to_dict() method.

Attributes:

Name Type Description
in_expand bool

Include class name in object expansion (during attribute extraction)

in_to_dict bool

Inject class name into to_dict() method results

key str

Dictionary key to use for class name (default: 'class_name')

fully_qualified bool

Use 'module.ClassName' vs 'ClassName' format

Examples:

>>> # Minimal class names for debugging
>>> opts = ClassNameOptions(in_expand=True, fully_qualified=False)
>>> # Full qualification for serialization
>>> opts = ClassNameOptions(
...     in_expand=True,
...     in_to_dict=True,
...     fully_qualified=True
... )
>>> # Custom key to avoid collisions
>>> opts = ClassNameOptions(in_expand=True, key='@type')
Source code in c108/dictify.py
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
@dataclass
class ClassNameOptions:
    """
    Configuration for class name injection in objects converted to mappings.

    Controls how and where class names appear in the output, useful for
    object reconstruction, debugging, and type tracking.

    Class name injection can be used only for an object represented as mapping when processed with dictify(),
    this includes but is not limited to objects implementing to_dict() method.

    Attributes:
        in_expand: Include class name in object expansion (during attribute extraction)
        in_to_dict: Inject class name into to_dict() method results
        key: Dictionary key to use for class name (default: '__class_name__')
        fully_qualified: Use 'module.ClassName' vs 'ClassName' format

    Examples:
        >>> # Minimal class names for debugging
        >>> opts = ClassNameOptions(in_expand=True, fully_qualified=False)

        >>> # Full qualification for serialization
        >>> opts = ClassNameOptions(
        ...     in_expand=True,
        ...     in_to_dict=True,
        ...     fully_qualified=True
        ... )

        >>> # Custom key to avoid collisions
        >>> opts = ClassNameOptions(in_expand=True, key='@type')
    """

    in_expand: bool = False
    in_to_dict: bool = False
    key: str = "__class_name__"
    fully_qualified: bool = False

    def merge(
        self,
        *,
        # Convenience parameter
        inject_class_name: bool = UNSET,
        # Direct attributes
        in_expand: bool = UNSET,
        in_to_dict: bool = UNSET,
        key: str = UNSET,
        fully_qualified: bool = UNSET,
    ) -> Self:
        """
        Create a new instance with merged configuration options.

        Use either the convenience parameter or the associated explicit attributes, but not both in the same call.
        Unspecified parameters retain their original values.

        Convenience Parameter:
            inject_class_name:
                - True: set both in_expand and in_to_dict to True (unless explicitly overridden)
                - False: set both in_expand and in_to_dict to False (unless explicitly overridden)
                - Cannot be used with: in_expand or in_to_dict

        Explicit Attributes:
            in_expand: Include class name in object expansion
            in_to_dict: Inject class name into to_dict() results
            key: Dictionary key for class name
            fully_qualified: Use fully qualified class names

        Returns:
            New ClassNameOptions instance with merged configuration

        Raises:
            ValueError: if both convenience parameter and explicit attributes are used in the same call.

        Examples:
            >>> # Enable class name injection everywhere
            >>> opts = ClassNameOptions()
            >>> opts = opts.merge(inject_class_name=True)

            >>> # Disable only in_to_dict while keeping in_expand
            >>> opts = opts.merge(in_to_dict=False)

            >>> # Change key and format
            >>> opts = opts.merge(key='@type', fully_qualified=True)

            >>> # Chaining
            >>> opts = (ClassNameOptions()
            ...     .merge(inject_class_name=True)
            ...     .merge(fully_qualified=True))
        """

        # Start from current values
        merged_in_expand = self.in_expand
        merged_in_to_dict = self.in_to_dict

        if inject_class_name is not UNSET:
            # Apply convenience parameter
            if in_expand is not UNSET:
                raise ValueError(
                    "cannot specify both inject_class_name and in_expand, use only one of them."
                )
            if in_to_dict is not UNSET:
                raise ValueError(
                    "cannot specify both inject_class_name and in_to_dict, use only one of them."
                )

            merged_in_expand = bool(inject_class_name)
            merged_in_to_dict = bool(inject_class_name)
        else:
            # Apply Explicit attributes
            merged_in_expand = ifnotunset(in_expand, default=merged_in_expand)
            merged_in_to_dict = ifnotunset(in_to_dict, default=merged_in_to_dict)

        return self.__class__(
            in_expand=merged_in_expand,
            in_to_dict=merged_in_to_dict,
            key=ifnotunset(key, default=self.key),
            fully_qualified=self.fully_qualified if fully_qualified is UNSET else fully_qualified,
        )

merge(*, inject_class_name=UNSET, in_expand=UNSET, in_to_dict=UNSET, key=UNSET, fully_qualified=UNSET)

Create a new instance with merged configuration options.

Use either the convenience parameter or the associated explicit attributes, but not both in the same call. Unspecified parameters retain their original values.

Convenience Parameter

inject_class_name: - True: set both in_expand and in_to_dict to True (unless explicitly overridden) - False: set both in_expand and in_to_dict to False (unless explicitly overridden) - Cannot be used with: in_expand or in_to_dict

Explicit Attributes

in_expand: Include class name in object expansion in_to_dict: Inject class name into to_dict() results key: Dictionary key for class name fully_qualified: Use fully qualified class names

Returns:

Type Description
Self

New ClassNameOptions instance with merged configuration

Raises:

Type Description
ValueError

if both convenience parameter and explicit attributes are used in the same call.

Examples:

>>> # Enable class name injection everywhere
>>> opts = ClassNameOptions()
>>> opts = opts.merge(inject_class_name=True)
>>> # Disable only in_to_dict while keeping in_expand
>>> opts = opts.merge(in_to_dict=False)
>>> # Change key and format
>>> opts = opts.merge(key='@type', fully_qualified=True)
>>> # Chaining
>>> opts = (ClassNameOptions()
...     .merge(inject_class_name=True)
...     .merge(fully_qualified=True))
Source code in c108/dictify.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def merge(
    self,
    *,
    # Convenience parameter
    inject_class_name: bool = UNSET,
    # Direct attributes
    in_expand: bool = UNSET,
    in_to_dict: bool = UNSET,
    key: str = UNSET,
    fully_qualified: bool = UNSET,
) -> Self:
    """
    Create a new instance with merged configuration options.

    Use either the convenience parameter or the associated explicit attributes, but not both in the same call.
    Unspecified parameters retain their original values.

    Convenience Parameter:
        inject_class_name:
            - True: set both in_expand and in_to_dict to True (unless explicitly overridden)
            - False: set both in_expand and in_to_dict to False (unless explicitly overridden)
            - Cannot be used with: in_expand or in_to_dict

    Explicit Attributes:
        in_expand: Include class name in object expansion
        in_to_dict: Inject class name into to_dict() results
        key: Dictionary key for class name
        fully_qualified: Use fully qualified class names

    Returns:
        New ClassNameOptions instance with merged configuration

    Raises:
        ValueError: if both convenience parameter and explicit attributes are used in the same call.

    Examples:
        >>> # Enable class name injection everywhere
        >>> opts = ClassNameOptions()
        >>> opts = opts.merge(inject_class_name=True)

        >>> # Disable only in_to_dict while keeping in_expand
        >>> opts = opts.merge(in_to_dict=False)

        >>> # Change key and format
        >>> opts = opts.merge(key='@type', fully_qualified=True)

        >>> # Chaining
        >>> opts = (ClassNameOptions()
        ...     .merge(inject_class_name=True)
        ...     .merge(fully_qualified=True))
    """

    # Start from current values
    merged_in_expand = self.in_expand
    merged_in_to_dict = self.in_to_dict

    if inject_class_name is not UNSET:
        # Apply convenience parameter
        if in_expand is not UNSET:
            raise ValueError(
                "cannot specify both inject_class_name and in_expand, use only one of them."
            )
        if in_to_dict is not UNSET:
            raise ValueError(
                "cannot specify both inject_class_name and in_to_dict, use only one of them."
            )

        merged_in_expand = bool(inject_class_name)
        merged_in_to_dict = bool(inject_class_name)
    else:
        # Apply Explicit attributes
        merged_in_expand = ifnotunset(in_expand, default=merged_in_expand)
        merged_in_to_dict = ifnotunset(in_to_dict, default=merged_in_to_dict)

    return self.__class__(
        in_expand=merged_in_expand,
        in_to_dict=merged_in_to_dict,
        key=ifnotunset(key, default=self.key),
        fully_qualified=self.fully_qualified if fully_qualified is UNSET else fully_qualified,
    )

DictifyOptions dataclass

Advanced configuration options for object-to-dictionary conversion with extensive customization.

Provides comprehensive control over object serialization including recursion depth management, attribute filtering, size constraints, custom type handling, and collection processing behavior. Supports both debugging and production serialization scenarios with flexible hook systems.

Core Configuration

max_depth: Maximum recursion depth for nested objects (default: 3) - max_depth < 0: Raw mode, uses handlers.raw handler - max_depth = 0: Terminal mode, uses handlers.terminal handler - max_depth > 0: Normal recursive processing

Attribute Control
  • include_none_attrs: Include object attributes with None values
  • include_none_items: Include mapping items with None values
  • include_private: Include private attributes (starting with _)
  • include_properties: Include instance properties with assigned values
Processing Handlers

handlers: Handlers provide processors for edge cases, mutability, and metadata injection - handlers.inject_meta: Handler for injecting metadata into objects - handlers.raw: Handler for raw mode (max_depth < 0) Fallback chain: handlers.raw() → obj.to_dict() → identity - handlers.terminal: Handler for terminal mode (max_depth = 0) Fallback chain: handlers.terminal() → type_handlers → obj.to_dict() → identity - handlers.expand: Handler for recursive mode expansion (max_depth >= 1) from object to a mutable collection

Size and Performance Limits
  • max_items: Maximum items in collections before trimming (default: 100). None = no limit (process entire collection).
  • max_str_len: String truncation limit (default: 200), None = no truncation
  • max_bytes: Bytes object truncation limit (default: 512), None = no truncation
Mapping keys and Iterable values handling

sort_iterables: Enable items sorting for iterables sort_keys: Enable key sorting for mappings

Class Name Injection

class_name: ClassNameOptions controlling class name appearance: - class_name.fully_qualified: Use 'module.Class' vs 'Class' format - class_name.in_expand: Add class name during object expansion - class_name.in_to_dict: Add class name to to_dict() results - class_name.key: Dictionary key for class name (default: 'class_name')

Reference Tracking

track_references: Enable cycle detection and reference tracking (see also MetaOptions.id)

Advanced Processing
  • hook_mode: Object conversion strategy: - "dict": Try to_dict() with fallback to recursive expansion - "dict_strict": Require to_dict() method (raises if missing) - "none": Skip object hooks, use expansion only
  • skip_types: Types bypassing all filtering (default: int, float, bool, complex, None)
  • type_handlers: Custom type processing functions with inheritance support
Type Handler System
  • Used for non-recursive processing of primitives; can be expanded with user data types
  • Supports exact type matching and inheritance-based resolution via MRO
  • Default handlers for: str, bytes, bytearray, memoryview
  • Precedence: type_handlers → obj.to_dict() → recursive expansion
  • Handlers receive (obj, options) and return processed result
Collection Processing Features
  • Comprehensive support for Sequences, Mappings, Sets, and MappingViews
  • Automatic trimming with metadata injection for oversized collections
  • Dict-like object detection and processing via items() method
Class Methods
  • debug(): Comprehensive debugging with shallow depth and all attributes
  • logging(): Controlled verbosity with size limits and metadata injection
  • serial(): Clean JSON-ready output with class names for reconstruction
Instance Methods
  • add_type_handler(typ, handler): Register custom type processor (chainable)
  • get_type_handler(obj): Retrieve handler via inheritance resolution
  • remove_type_handler(typ): Unregister type processor (chainable)
Properties
  • type_handlers: Dict[Type, Callable] - getter/setter with validation

Examples:

>>> # Basic usage with defaults
>>> class Obj:
...     a: int = 1
...     b: str = "2"
>>> o = Obj()
>>> dictify(o)
{'a': 1, 'b': '2'}
>>> # Debugging
>>> dictify(o, opts=DictifyOptions.debug())
{'a': 1, 'b': '2', '__dictify__': {'type': {'from_type': <class 'c108.dictify.Obj'>, 'to_type': <class 'dict'>}, 'VERSION': 1}}
>>> # Custom type handlers with method chaining
>>> import socket, threading
>>> options = (
...     DictifyOptions()
...     .add_type_handler(socket.socket,
...                      lambda s, opts: {"type": "socket", "closed": s._closed})
...     .add_type_handler(threading.Thread,
...                      lambda t, opts: {"name": t.name, "alive": t.is_alive()})
... )
>>> # Custom handlers without defaults
>>> minimal_opts = (
...     DictifyOptions(type_handlers={})  # Empty dict = no default handlers
...     .add_type_handler(str, lambda s, opts: s.upper())
...     .add_type_handler(dict, lambda d, opts: f"<dict:{len(d)} items>")
... )
>>> # Size-constrained processing
>>> constrained = DictifyOptions(
...     max_items=50,           # Trim large collections
...     max_str_len=100,     # Truncate long strings
...     max_bytes=512          # Limit byte arrays
... )
>>> # Deep inspection with custom terminal handler
>>> def custom_terminal(obj, opts):
...     return f"<{type(obj).__name__} at depth limit>"
>>>
>>> deep_opts = DictifyOptions(
...     max_depth=10,
...     handlers=Handlers(terminal=custom_terminal)  # Updated syntax
... )
Processing Order
  1. Skip types (int, float, bool, complex, None) → return as-is
  2. Edge cases (max_depth < 0 or == 0) → use handlers.raw/handlers.terminal chains
  3. Type handlers → custom processing
  4. Object hooks (to_dict()) → if available and hook_mode allows
  5. Collection processing → sequences, mappings, sets, views
  6. Object expansion → convert to dict with attribute filtering
Notes
  • All size limits apply during processing with automatic truncation
  • MRO-based inheritance resolution for type handlers
  • Properties raising exceptions are automatically skipped
  • Class name injection only affects main processing, not edge case handlers
  • Collection trimming injects metadata mapped from DictifyOptions.meta.key or as the last sequence element
Source code in c108/dictify.py
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
@dataclass
class DictifyOptions:
    """
    Advanced configuration options for object-to-dictionary conversion with extensive customization.

    Provides comprehensive control over object serialization including recursion depth management,
    attribute filtering, size constraints, custom type handling, and collection processing behavior.
    Supports both debugging and production serialization scenarios with flexible hook systems.

    Core Configuration:
        max_depth: Maximum recursion depth for nested objects (default: 3)
                  - max_depth < 0: Raw mode, uses handlers.raw handler
                  - max_depth = 0: Terminal mode, uses handlers.terminal handler
                  - max_depth > 0: Normal recursive processing

    Attribute Control:
        - include_none_attrs: Include object attributes with None values
        - include_none_items: Include mapping items with None values
        - include_private: Include private attributes (starting with _)
        - include_properties: Include instance properties with assigned values

    Processing Handlers:
        handlers: Handlers provide processors for edge cases, mutability, and metadata injection
            - handlers.inject_meta: Handler for injecting metadata into objects
            - handlers.raw: Handler for raw mode (max_depth < 0)
                            Fallback chain: handlers.raw() → obj.to_dict() → identity
            - handlers.terminal: Handler for terminal mode (max_depth = 0)
                                 Fallback chain: handlers.terminal() → type_handlers → obj.to_dict() → identity
            - handlers.expand: Handler for recursive mode expansion (max_depth >= 1)
                               from object to a mutable collection

    Size and Performance Limits:
        - max_items: Maximum items in collections before trimming (default: 100).
                     None = no limit (process entire collection).
        - max_str_len: String truncation limit (default: 200), None = no truncation
        - max_bytes: Bytes object truncation limit (default: 512), None = no truncation

    Mapping keys and Iterable values handling:
        sort_iterables: Enable items sorting for iterables
        sort_keys: Enable key sorting for mappings

    Class Name Injection:
        class_name: ClassNameOptions controlling class name appearance:
            - class_name.fully_qualified: Use 'module.Class' vs 'Class' format
            - class_name.in_expand: Add class name during object expansion
            - class_name.in_to_dict: Add class name to to_dict() results
            - class_name.key: Dictionary key for class name (default: '__class_name__')

    Meta Data Injection:
        meta: MetaOptions controlling what metadata gets injected:
              - meta.trim: Trimming statistics for oversized collections
              - meta.type: Type conversion information
              - meta.len/size/deep_size: Object size metadata
              - meta.key: Dictionary key for metadata in mappings

    Reference Tracking:
        track_references: Enable cycle detection and reference tracking (see also MetaOptions.id)

    Advanced Processing:
        - hook_mode: Object conversion strategy:
                  - "dict": Try to_dict() with fallback to recursive expansion
                  - "dict_strict": Require to_dict() method (raises if missing)
                  - "none": Skip object hooks, use expansion only
        - skip_types: Types bypassing all filtering (default: int, float, bool, complex, None)
        - type_handlers: Custom type processing functions with inheritance support

    Type Handler System:
        - Used for non-recursive processing of primitives; can be expanded with user data types
        - Supports exact type matching and inheritance-based resolution via MRO
        - Default handlers for: str, bytes, bytearray, memoryview
        - Precedence: type_handlers → obj.to_dict() → recursive expansion
        - Handlers receive (obj, options) and return processed result

    Collection Processing Features:
        - Comprehensive support for Sequences, Mappings, Sets, and MappingViews
        - Automatic trimming with metadata injection for oversized collections
        - Dict-like object detection and processing via items() method

    Class Methods:
        - debug(): Comprehensive debugging with shallow depth and all attributes
        - logging(): Controlled verbosity with size limits and metadata injection
        - serial(): Clean JSON-ready output with class names for reconstruction

    Instance Methods:
        - add_type_handler(typ, handler): Register custom type processor (chainable)
        - get_type_handler(obj): Retrieve handler via inheritance resolution
        - remove_type_handler(typ): Unregister type processor (chainable)

    Properties:
        - type_handlers: Dict[Type, Callable] - getter/setter with validation

    Examples:
        >>> # Basic usage with defaults
        >>> class Obj:
        ...     a: int = 1
        ...     b: str = "2"
        >>> o = Obj()
        >>> dictify(o)
        {'a': 1, 'b': '2'}

        >>> # Debugging
        >>> dictify(o, opts=DictifyOptions.debug())
        {'a': 1, 'b': '2', '__dictify__': {'type': {'from_type': <class 'c108.dictify.Obj'>, 'to_type': <class 'dict'>}, 'VERSION': 1}}

        >>> # Custom type handlers with method chaining
        >>> import socket, threading
        >>> options = (
        ...     DictifyOptions()
        ...     .add_type_handler(socket.socket,
        ...                      lambda s, opts: {"type": "socket", "closed": s._closed})
        ...     .add_type_handler(threading.Thread,
        ...                      lambda t, opts: {"name": t.name, "alive": t.is_alive()})
        ... )

        >>> # Custom handlers without defaults
        >>> minimal_opts = (
        ...     DictifyOptions(type_handlers={})  # Empty dict = no default handlers
        ...     .add_type_handler(str, lambda s, opts: s.upper())
        ...     .add_type_handler(dict, lambda d, opts: f"<dict:{len(d)} items>")
        ... )

        >>> # Size-constrained processing
        >>> constrained = DictifyOptions(
        ...     max_items=50,           # Trim large collections
        ...     max_str_len=100,     # Truncate long strings
        ...     max_bytes=512          # Limit byte arrays
        ... )

        >>> # Deep inspection with custom terminal handler
        >>> def custom_terminal(obj, opts):
        ...     return f"<{type(obj).__name__} at depth limit>"
        >>>
        >>> deep_opts = DictifyOptions(
        ...     max_depth=10,
        ...     handlers=Handlers(terminal=custom_terminal)  # Updated syntax
        ... )

    Processing Order:
        1. Skip types (int, float, bool, complex, None) → return as-is
        2. Edge cases (max_depth < 0 or == 0) → use handlers.raw/handlers.terminal chains
        3. Type handlers → custom processing
        4. Object hooks (to_dict()) → if available and hook_mode allows
        5. Collection processing → sequences, mappings, sets, views
        6. Object expansion → convert to dict with attribute filtering

    Notes:
        - All size limits apply during processing with automatic truncation
        - MRO-based inheritance resolution for type handlers
        - Properties raising exceptions are automatically skipped
        - Class name injection only affects main processing, not edge case handlers
        - Collection trimming injects metadata mapped from DictifyOptions.meta.key or as the last sequence element
    """

    max_depth: int = 3

    include_none_attrs: bool = False
    include_none_items: bool = False
    include_private: bool = False
    include_properties: bool = False

    # Processing handlers
    handlers: Handlers = field(default_factory=Handlers)

    # Size limits
    max_items: int | None = 100
    max_str_len: int | None = 200
    max_bytes: int | None = 512

    # Mapping Keys handling
    sort_keys: bool = False
    sort_iterables: bool = False

    # Class Name Injection
    class_name: ClassNameOptions = field(default_factory=ClassNameOptions)

    # Meta Data Injection
    meta: MetaOptions = field(default_factory=MetaOptions)

    # Reference tracking and cycle detection
    track_references: bool = True

    # Advanced
    hook_mode: str = HookMode.DICT
    skip_types: tuple[type, ...] = (int, float, bool, complex, type(None))

    type_handlers: Dict[Type, Callable[[Any, "DictifyOptions"], Any]] = field(
        default_factory=lambda: DictifyOptions.default_type_handlers()
    )

    def __post_init__(self) -> None:
        """Validate field values after dataclass initialization."""
        # Validate max_depth
        if not isinstance(self.max_depth, int):
            raise TypeError(f"max_depth must be int, got {fmt_type(self.max_depth)}")

        # Validate size limits
        for name in ("max_items", "max_str_len", "max_bytes"):
            # Should be None for NO LIMIT or int >=0
            val = getattr(self, name)
            if val is not None and (not isinstance(val, int) or val < 0):
                raise ValueError(f"{name} must be None or non-negative int, got {val!r}")

        # Validate handlers
        if not isinstance(self.handlers, Handlers):
            raise TypeError(f"handlers must be Handlers, got {fmt_type(self.handlers)}")

        # Validate class_name
        if not isinstance(self.class_name, ClassNameOptions):
            raise TypeError(f"class_name must be ClassNameOptions, got {fmt_type(self.class_name)}")

        # Validate meta
        if not isinstance(self.meta, MetaOptions):
            raise TypeError(f"meta must be MetaOptions, got {fmt_type(self.meta)}")

        # Validate hook_mode
        if not isinstance(self.hook_mode, str):
            raise TypeError(f"hook_mode must be str, got {fmt_type(self.hook_mode)}")
        if self.hook_mode not in (HookMode.DICT, HookMode.DICT_STRICT, HookMode.NONE):
            raise ValueError(f"Invalid hook_mode: {self.hook_mode}")

        # Validate skip_types
        if not isinstance(self.skip_types, tuple) or not all(
            isinstance(t, type) for t in self.skip_types
        ):
            raise TypeError("skip_types must be a tuple of types")

        # Validate type_handlers
        if not isinstance(self.type_handlers, dict):
            raise TypeError(f"type_handlers must be dict, got {fmt_type(self.type_handlers)}")
        for k, v in self.type_handlers.items():
            if not isinstance(k, type):
                raise TypeError(f"type_handlers key must be type, got {fmt_type(k)}")
            if not callable(v):
                raise TypeError(
                    f"type_handlers value for {fmt_type(k)} must be callable, got {fmt_type(v)}"
                )

    # Static Methods -----------------------------------

    @staticmethod
    def default_type_handlers() -> Dict[Type, Callable]:
        """
        Get default type handlers for commonly filtered types.

        Returns:
            Dictionary mapping types to their default handler functions
        """
        return {
            BaseException: _handle_exception,
            Decimal: _handle_decimal,
            Enum: _handle_enum,
            Fraction: _handle_fraction,
            Path: _handle_path,
            UUID: _handle_uuid,
            bytearray: _handle_bytearray,
            bytes: _handle_bytes,
            date: _handle_date,
            datetime: _handle_datetime,
            memoryview: _handle_memoryview,
            range: _handle_range,
            re.Pattern: _handle_regex_pattern,
            str: _handle_str,
            time: _handle_time,
            timedelta: _handle_timedelta,
        }

    # Class Methods ------------------------------------

    @classmethod
    def debug(cls) -> Self:
        """
        Create a DictifyOptions instance configured for debugging.

        Shallow inspection showing everything including internals.
        No size limits to avoid data loss during debugging.

        Returns:
            DictifyOptions: Configuration optimized for debugging with shallow depth,
                           all attributes included, and minimal filtering.
        """
        return cls(
            max_depth=5,
            include_none_attrs=True,
            include_none_items=True,
            include_private=True,
            include_properties=True,
            max_items=256,
            max_str_len=1024,
            max_bytes=1024,
            sort_keys=True,
            class_name=ClassNameOptions(in_expand=True, in_to_dict=True, fully_qualified=True),
            meta=MetaOptions(
                in_expand=True,
                in_to_dict=True,
                trim=True,
                type=True,
                len=True,
                size=False,
                deep_size=False,
            ),
        )

    @classmethod
    def logging(cls) -> Self:
        """
        Create a DictifyOptions instance configured for logging.

        Controlled verbosity with size limits and metadata injection.
        Balanced between information and performance.

        Returns:
            DictifyOptions: Configuration for logging with controlled depth,
                           size limits, and helpful metadata injection.
        """
        return cls(
            max_depth=3,
            include_none_attrs=False,
            include_none_items=False,
            include_private=False,
            include_properties=True,
            max_items=64,
            max_str_len=128,
            max_bytes=128,
            sort_keys=True,
            class_name=ClassNameOptions(in_expand=True, in_to_dict=True, fully_qualified=True),
            meta=MetaOptions(
                in_expand=True,
                in_to_dict=True,
                trim=True,
                type=True,
                len=True,
                size=True,
                deep_size=False,
            ),
        )

    @classmethod
    def serial(cls) -> Self:
        """
        Create a DictifyOptions instance configured for serialization.

        Clean output optimized for JSON serialization and reconstruction.
        No None values, includes class names for type reconstruction.

        Returns:
            DictifyOptions: Configuration for serialization with class names
                           included, clean output, and JSON-friendly formatting.
        """
        return cls(
            max_depth=5,
            include_none_attrs=False,
            include_none_items=False,
            include_private=False,
            include_properties=False,
            max_items=1024,
            max_str_len=1024,
            max_bytes=2048,
            sort_keys=True,
            hook_mode=HookMode.DICT_STRICT,
            class_name=ClassNameOptions(in_expand=True, in_to_dict=True, fully_qualified=True),
            meta=MetaOptions(
                in_expand=False,
                in_to_dict=False,
                trim=False,
                type=False,
                len=False,
                size=False,
                deep_size=False,
            ),
        )

    # Methods and Properties ---------------------

    def add_type_handler(
        self,
        typ: type,
        handler: Callable[[Any, "DictifyOptions"], Any],
    ) -> Self:
        """
        Register or override a handler for a specific type.

        Args:
            typ: The concrete type to process.
            handler: A callable receiving (obj, options) and returning processed value.
        """
        self.type_handlers[typ] = handler
        return self

    def get_type_handler(self, obj: Any) -> abc.Callable[[Any, Self], Any] | None:
        """
        Get the handler function for the object's type (exact or via inheritance).

        Searches for the nearest ancestor via MRO; if ancestors not found, returns
        exact type match or None.

        Args:
            obj: Object to potentially handle.
            options: DictifyOptions instance.

        Returns:
            The handler function if found; otherwise None.
        """
        obj_type = type(obj)
        type_handlers = self.type_handlers

        # Fast path: exact type match
        if obj_type in type_handlers:
            return type_handlers[obj_type]

        # Build candidates that are supertypes of obj_type (robust to non-type keys)
        handler_type_keys = [k for k in type_handlers.keys() if isinstance(k, type)]
        candidates: list[type] = []
        for handler_type in handler_type_keys:
            try:
                if handler_type is not obj_type and issubclass(obj_type, handler_type):
                    candidates.append(handler_type)
            except TypeError:
                # Skip keys that aren't valid types
                continue

        # Prefer the nearest ancestor using the MRO
        if candidates:
            for base in obj_type.__mro__[1:]:
                if base in candidates:
                    return type_handlers[base]

            # Search exact type match
            for k in type_handlers.keys():
                if k in candidates:
                    return type_handlers[k]

        return None

    def merge(
        self,
        *,
        # Common explicit attributes
        max_depth: int = UNSET,
        max_items: int | None = UNSET,
        max_str_len: int | None = UNSET,
        max_bytes: int | None = UNSET,
        include_none_attrs: bool = UNSET,
        include_none_items: bool = UNSET,
        include_private: bool = UNSET,
        include_properties: bool = UNSET,
        sort_keys: bool = UNSET,
        sort_iterables: bool = UNSET,
        # Convenience parameters (affect multiple attributes)
        inject_class_name: bool = UNSET,
        inject_trim_meta: bool = UNSET,
        inject_type_meta: bool = UNSET,
        # Advanced nested objects
        class_name: ClassNameOptions = UNSET,
        meta: MetaOptions = UNSET,
        handlers: Handlers = UNSET,
    ) -> Self:
        """
        Create a new instance with merged configuration options.

        Supports both high-level convenience parameters that affect multiple attributes
        and explicit attribute assignment. Unspecified parameters retain their current values.
        All parameters are optional, skip to keep current values

        Convenience Parameters:
            inject_class_name: When True, sets both class_name.in_expand and
                               class_name.in_to_dict to True. When False, sets both to False.
            inject_trim_meta: When True, enables meta.trim and injection. When False, disables meta.trim.
            inject_type_meta: When True, enables meta.type and injection. When False, disables meta.type.

        Precedence:
            If both convenience flags and explicit args are provided,
            explicit args override the convenience parameters.

        Common Direct Attributes:
            max_depth: Maximum recursion depth for nested objects
            max_items: Maximum items in collections before trimming or None for untrimmed
            max_str_len: String truncation limit or None for unlimited
            max_bytes: Bytes object truncation limit or None for unlimited
            include_none_attrs: Include object attributes with None values
            include_none_items: Include dictionary items with None values
            include_private: Include private attributes (starting with _)
            include_properties: Include instance properties with assigned values
            sort_keys: Enable key sorting for mappings
            sort_iterables: Enable items sorting for iterables

        Advanced Nested Objects:
            class_name: Complete ClassNameOptions instance for full control
            meta: Complete MetaOptions instance for full control
            handlers: Complete Handlers instance for custom processing

        Returns:
            New DictifyOptions instance with merged configuration
        """
        # Handle convenience parameters by creating modified nested objects
        merged_class_name = self.class_name
        merged_meta = self.meta

        # If explicit nested objects are provided, they should override convenience flags entirely.
        if class_name is not UNSET:
            merged_class_name = class_name
        else:
            if inject_class_name is not UNSET:
                merged_class_name = merged_class_name.merge(inject_class_name=inject_class_name)

        if meta is not UNSET:
            merged_meta = meta
        else:
            if inject_trim_meta is not UNSET:
                merged_meta = merged_meta.merge(trim=inject_trim_meta)
            if inject_type_meta is not UNSET:
                merged_meta = merged_meta.merge(type=inject_type_meta)

        # Build new instance with merged values
        return self.__class__(
            max_depth=_merge_new_default(max_depth, self.max_depth),
            include_none_attrs=_merge_new_default(include_none_attrs, self.include_none_attrs),
            include_none_items=_merge_new_default(include_none_items, self.include_none_items),
            include_private=_merge_new_default(include_private, self.include_private),
            include_properties=_merge_new_default(include_properties, self.include_properties),
            handlers=_merge_new_default(handlers, self.handlers),
            max_items=_merge_new_default(max_items, self.max_items),
            max_str_len=_merge_new_default(max_str_len, self.max_str_len),
            max_bytes=_merge_new_default(max_bytes, self.max_bytes),
            sort_keys=_merge_new_default(sort_keys, self.sort_keys),
            sort_iterables=_merge_new_default(sort_iterables, self.sort_iterables),
            class_name=merged_class_name,
            meta=merged_meta,
            hook_mode=self.hook_mode,
            skip_types=self.skip_types,
            type_handlers=self.type_handlers,
        )

    def remove_type_handler(self, typ: type) -> Self:
        """
        Remove a handler for a specific type.

        Args:
            typ: The concrete type to remove handler for.

        Returns:
            Self, to allow chaining.

        Raises:
            TypeError: If typ or type_handlers type is invalid.
        """
        if not isinstance(typ, type):
            raise TypeError(f"typ must be a type, got {fmt_type(typ)}")

        # Remove handler for the given type if it exists
        self.type_handlers.pop(typ, None)
        return self

__post_init__()

Validate field values after dataclass initialization.

Source code in c108/dictify.py
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
def __post_init__(self) -> None:
    """Validate field values after dataclass initialization."""
    # Validate max_depth
    if not isinstance(self.max_depth, int):
        raise TypeError(f"max_depth must be int, got {fmt_type(self.max_depth)}")

    # Validate size limits
    for name in ("max_items", "max_str_len", "max_bytes"):
        # Should be None for NO LIMIT or int >=0
        val = getattr(self, name)
        if val is not None and (not isinstance(val, int) or val < 0):
            raise ValueError(f"{name} must be None or non-negative int, got {val!r}")

    # Validate handlers
    if not isinstance(self.handlers, Handlers):
        raise TypeError(f"handlers must be Handlers, got {fmt_type(self.handlers)}")

    # Validate class_name
    if not isinstance(self.class_name, ClassNameOptions):
        raise TypeError(f"class_name must be ClassNameOptions, got {fmt_type(self.class_name)}")

    # Validate meta
    if not isinstance(self.meta, MetaOptions):
        raise TypeError(f"meta must be MetaOptions, got {fmt_type(self.meta)}")

    # Validate hook_mode
    if not isinstance(self.hook_mode, str):
        raise TypeError(f"hook_mode must be str, got {fmt_type(self.hook_mode)}")
    if self.hook_mode not in (HookMode.DICT, HookMode.DICT_STRICT, HookMode.NONE):
        raise ValueError(f"Invalid hook_mode: {self.hook_mode}")

    # Validate skip_types
    if not isinstance(self.skip_types, tuple) or not all(
        isinstance(t, type) for t in self.skip_types
    ):
        raise TypeError("skip_types must be a tuple of types")

    # Validate type_handlers
    if not isinstance(self.type_handlers, dict):
        raise TypeError(f"type_handlers must be dict, got {fmt_type(self.type_handlers)}")
    for k, v in self.type_handlers.items():
        if not isinstance(k, type):
            raise TypeError(f"type_handlers key must be type, got {fmt_type(k)}")
        if not callable(v):
            raise TypeError(
                f"type_handlers value for {fmt_type(k)} must be callable, got {fmt_type(v)}"
            )

add_type_handler(typ, handler)

Register or override a handler for a specific type.

Parameters:

Name Type Description Default
typ type

The concrete type to process.

required
handler Callable[[Any, DictifyOptions], Any]

A callable receiving (obj, options) and returning processed value.

required
Source code in c108/dictify.py
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
def add_type_handler(
    self,
    typ: type,
    handler: Callable[[Any, "DictifyOptions"], Any],
) -> Self:
    """
    Register or override a handler for a specific type.

    Args:
        typ: The concrete type to process.
        handler: A callable receiving (obj, options) and returning processed value.
    """
    self.type_handlers[typ] = handler
    return self

debug() classmethod

Create a DictifyOptions instance configured for debugging.

Shallow inspection showing everything including internals. No size limits to avoid data loss during debugging.

Returns:

Name Type Description
DictifyOptions Self

Configuration optimized for debugging with shallow depth, all attributes included, and minimal filtering.

Source code in c108/dictify.py
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
@classmethod
def debug(cls) -> Self:
    """
    Create a DictifyOptions instance configured for debugging.

    Shallow inspection showing everything including internals.
    No size limits to avoid data loss during debugging.

    Returns:
        DictifyOptions: Configuration optimized for debugging with shallow depth,
                       all attributes included, and minimal filtering.
    """
    return cls(
        max_depth=5,
        include_none_attrs=True,
        include_none_items=True,
        include_private=True,
        include_properties=True,
        max_items=256,
        max_str_len=1024,
        max_bytes=1024,
        sort_keys=True,
        class_name=ClassNameOptions(in_expand=True, in_to_dict=True, fully_qualified=True),
        meta=MetaOptions(
            in_expand=True,
            in_to_dict=True,
            trim=True,
            type=True,
            len=True,
            size=False,
            deep_size=False,
        ),
    )

default_type_handlers() staticmethod

Get default type handlers for commonly filtered types.

Returns:

Type Description
Dict[Type, Callable]

Dictionary mapping types to their default handler functions

Source code in c108/dictify.py
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
@staticmethod
def default_type_handlers() -> Dict[Type, Callable]:
    """
    Get default type handlers for commonly filtered types.

    Returns:
        Dictionary mapping types to their default handler functions
    """
    return {
        BaseException: _handle_exception,
        Decimal: _handle_decimal,
        Enum: _handle_enum,
        Fraction: _handle_fraction,
        Path: _handle_path,
        UUID: _handle_uuid,
        bytearray: _handle_bytearray,
        bytes: _handle_bytes,
        date: _handle_date,
        datetime: _handle_datetime,
        memoryview: _handle_memoryview,
        range: _handle_range,
        re.Pattern: _handle_regex_pattern,
        str: _handle_str,
        time: _handle_time,
        timedelta: _handle_timedelta,
    }

get_type_handler(obj)

Get the handler function for the object's type (exact or via inheritance).

Searches for the nearest ancestor via MRO; if ancestors not found, returns exact type match or None.

Parameters:

Name Type Description Default
obj Any

Object to potentially handle.

required
options

DictifyOptions instance.

required

Returns:

Type Description
Callable[[Any, Self], Any] | None

The handler function if found; otherwise None.

Source code in c108/dictify.py
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
def get_type_handler(self, obj: Any) -> abc.Callable[[Any, Self], Any] | None:
    """
    Get the handler function for the object's type (exact or via inheritance).

    Searches for the nearest ancestor via MRO; if ancestors not found, returns
    exact type match or None.

    Args:
        obj: Object to potentially handle.
        options: DictifyOptions instance.

    Returns:
        The handler function if found; otherwise None.
    """
    obj_type = type(obj)
    type_handlers = self.type_handlers

    # Fast path: exact type match
    if obj_type in type_handlers:
        return type_handlers[obj_type]

    # Build candidates that are supertypes of obj_type (robust to non-type keys)
    handler_type_keys = [k for k in type_handlers.keys() if isinstance(k, type)]
    candidates: list[type] = []
    for handler_type in handler_type_keys:
        try:
            if handler_type is not obj_type and issubclass(obj_type, handler_type):
                candidates.append(handler_type)
        except TypeError:
            # Skip keys that aren't valid types
            continue

    # Prefer the nearest ancestor using the MRO
    if candidates:
        for base in obj_type.__mro__[1:]:
            if base in candidates:
                return type_handlers[base]

        # Search exact type match
        for k in type_handlers.keys():
            if k in candidates:
                return type_handlers[k]

    return None

logging() classmethod

Create a DictifyOptions instance configured for logging.

Controlled verbosity with size limits and metadata injection. Balanced between information and performance.

Returns:

Name Type Description
DictifyOptions Self

Configuration for logging with controlled depth, size limits, and helpful metadata injection.

Source code in c108/dictify.py
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
@classmethod
def logging(cls) -> Self:
    """
    Create a DictifyOptions instance configured for logging.

    Controlled verbosity with size limits and metadata injection.
    Balanced between information and performance.

    Returns:
        DictifyOptions: Configuration for logging with controlled depth,
                       size limits, and helpful metadata injection.
    """
    return cls(
        max_depth=3,
        include_none_attrs=False,
        include_none_items=False,
        include_private=False,
        include_properties=True,
        max_items=64,
        max_str_len=128,
        max_bytes=128,
        sort_keys=True,
        class_name=ClassNameOptions(in_expand=True, in_to_dict=True, fully_qualified=True),
        meta=MetaOptions(
            in_expand=True,
            in_to_dict=True,
            trim=True,
            type=True,
            len=True,
            size=True,
            deep_size=False,
        ),
    )

merge(*, max_depth=UNSET, max_items=UNSET, max_str_len=UNSET, max_bytes=UNSET, include_none_attrs=UNSET, include_none_items=UNSET, include_private=UNSET, include_properties=UNSET, sort_keys=UNSET, sort_iterables=UNSET, inject_class_name=UNSET, inject_trim_meta=UNSET, inject_type_meta=UNSET, class_name=UNSET, meta=UNSET, handlers=UNSET)

Create a new instance with merged configuration options.

Supports both high-level convenience parameters that affect multiple attributes and explicit attribute assignment. Unspecified parameters retain their current values. All parameters are optional, skip to keep current values

Convenience Parameters

inject_class_name: When True, sets both class_name.in_expand and class_name.in_to_dict to True. When False, sets both to False. inject_trim_meta: When True, enables meta.trim and injection. When False, disables meta.trim. inject_type_meta: When True, enables meta.type and injection. When False, disables meta.type.

Precedence

If both convenience flags and explicit args are provided, explicit args override the convenience parameters.

Common Direct Attributes

max_depth: Maximum recursion depth for nested objects max_items: Maximum items in collections before trimming or None for untrimmed max_str_len: String truncation limit or None for unlimited max_bytes: Bytes object truncation limit or None for unlimited include_none_attrs: Include object attributes with None values include_none_items: Include dictionary items with None values include_private: Include private attributes (starting with _) include_properties: Include instance properties with assigned values sort_keys: Enable key sorting for mappings sort_iterables: Enable items sorting for iterables

Advanced Nested Objects

class_name: Complete ClassNameOptions instance for full control meta: Complete MetaOptions instance for full control handlers: Complete Handlers instance for custom processing

Returns:

Type Description
Self

New DictifyOptions instance with merged configuration

Source code in c108/dictify.py
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
def merge(
    self,
    *,
    # Common explicit attributes
    max_depth: int = UNSET,
    max_items: int | None = UNSET,
    max_str_len: int | None = UNSET,
    max_bytes: int | None = UNSET,
    include_none_attrs: bool = UNSET,
    include_none_items: bool = UNSET,
    include_private: bool = UNSET,
    include_properties: bool = UNSET,
    sort_keys: bool = UNSET,
    sort_iterables: bool = UNSET,
    # Convenience parameters (affect multiple attributes)
    inject_class_name: bool = UNSET,
    inject_trim_meta: bool = UNSET,
    inject_type_meta: bool = UNSET,
    # Advanced nested objects
    class_name: ClassNameOptions = UNSET,
    meta: MetaOptions = UNSET,
    handlers: Handlers = UNSET,
) -> Self:
    """
    Create a new instance with merged configuration options.

    Supports both high-level convenience parameters that affect multiple attributes
    and explicit attribute assignment. Unspecified parameters retain their current values.
    All parameters are optional, skip to keep current values

    Convenience Parameters:
        inject_class_name: When True, sets both class_name.in_expand and
                           class_name.in_to_dict to True. When False, sets both to False.
        inject_trim_meta: When True, enables meta.trim and injection. When False, disables meta.trim.
        inject_type_meta: When True, enables meta.type and injection. When False, disables meta.type.

    Precedence:
        If both convenience flags and explicit args are provided,
        explicit args override the convenience parameters.

    Common Direct Attributes:
        max_depth: Maximum recursion depth for nested objects
        max_items: Maximum items in collections before trimming or None for untrimmed
        max_str_len: String truncation limit or None for unlimited
        max_bytes: Bytes object truncation limit or None for unlimited
        include_none_attrs: Include object attributes with None values
        include_none_items: Include dictionary items with None values
        include_private: Include private attributes (starting with _)
        include_properties: Include instance properties with assigned values
        sort_keys: Enable key sorting for mappings
        sort_iterables: Enable items sorting for iterables

    Advanced Nested Objects:
        class_name: Complete ClassNameOptions instance for full control
        meta: Complete MetaOptions instance for full control
        handlers: Complete Handlers instance for custom processing

    Returns:
        New DictifyOptions instance with merged configuration
    """
    # Handle convenience parameters by creating modified nested objects
    merged_class_name = self.class_name
    merged_meta = self.meta

    # If explicit nested objects are provided, they should override convenience flags entirely.
    if class_name is not UNSET:
        merged_class_name = class_name
    else:
        if inject_class_name is not UNSET:
            merged_class_name = merged_class_name.merge(inject_class_name=inject_class_name)

    if meta is not UNSET:
        merged_meta = meta
    else:
        if inject_trim_meta is not UNSET:
            merged_meta = merged_meta.merge(trim=inject_trim_meta)
        if inject_type_meta is not UNSET:
            merged_meta = merged_meta.merge(type=inject_type_meta)

    # Build new instance with merged values
    return self.__class__(
        max_depth=_merge_new_default(max_depth, self.max_depth),
        include_none_attrs=_merge_new_default(include_none_attrs, self.include_none_attrs),
        include_none_items=_merge_new_default(include_none_items, self.include_none_items),
        include_private=_merge_new_default(include_private, self.include_private),
        include_properties=_merge_new_default(include_properties, self.include_properties),
        handlers=_merge_new_default(handlers, self.handlers),
        max_items=_merge_new_default(max_items, self.max_items),
        max_str_len=_merge_new_default(max_str_len, self.max_str_len),
        max_bytes=_merge_new_default(max_bytes, self.max_bytes),
        sort_keys=_merge_new_default(sort_keys, self.sort_keys),
        sort_iterables=_merge_new_default(sort_iterables, self.sort_iterables),
        class_name=merged_class_name,
        meta=merged_meta,
        hook_mode=self.hook_mode,
        skip_types=self.skip_types,
        type_handlers=self.type_handlers,
    )

remove_type_handler(typ)

Remove a handler for a specific type.

Parameters:

Name Type Description Default
typ type

The concrete type to remove handler for.

required

Returns:

Type Description
Self

Self, to allow chaining.

Raises:

Type Description
TypeError

If typ or type_handlers type is invalid.

Source code in c108/dictify.py
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
def remove_type_handler(self, typ: type) -> Self:
    """
    Remove a handler for a specific type.

    Args:
        typ: The concrete type to remove handler for.

    Returns:
        Self, to allow chaining.

    Raises:
        TypeError: If typ or type_handlers type is invalid.
    """
    if not isinstance(typ, type):
        raise TypeError(f"typ must be a type, got {fmt_type(typ)}")

    # Remove handler for the given type if it exists
    self.type_handlers.pop(typ, None)
    return self

serial() classmethod

Create a DictifyOptions instance configured for serialization.

Clean output optimized for JSON serialization and reconstruction. No None values, includes class names for type reconstruction.

Returns:

Name Type Description
DictifyOptions Self

Configuration for serialization with class names included, clean output, and JSON-friendly formatting.

Source code in c108/dictify.py
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
@classmethod
def serial(cls) -> Self:
    """
    Create a DictifyOptions instance configured for serialization.

    Clean output optimized for JSON serialization and reconstruction.
    No None values, includes class names for type reconstruction.

    Returns:
        DictifyOptions: Configuration for serialization with class names
                       included, clean output, and JSON-friendly formatting.
    """
    return cls(
        max_depth=5,
        include_none_attrs=False,
        include_none_items=False,
        include_private=False,
        include_properties=False,
        max_items=1024,
        max_str_len=1024,
        max_bytes=2048,
        sort_keys=True,
        hook_mode=HookMode.DICT_STRICT,
        class_name=ClassNameOptions(in_expand=True, in_to_dict=True, fully_qualified=True),
        meta=MetaOptions(
            in_expand=False,
            in_to_dict=False,
            trim=False,
            type=False,
            len=False,
            size=False,
            deep_size=False,
        ),
    )

Handlers dataclass

Processing handlers for different conversion stages.

Handlers

expand: Recursive convertor for expanding the topmost level of the object's tree. Processing chain: skip_types → raw()/terminal() → type_handlers → obj.to_dict() → expand() inject_meta: Inject serialization metadata into a processed object; to be called within expand() and after to_dict() processing raw: Custom handler for raw processing mode (max_depth < 0). Fallback chain: raw() → obj.to_dict() → identity function terminal: Custom handler for terminal processing (max_depth = 0). Fallback chain: terminal() → type_handlers → obj.to_dict() → identity

Source code in c108/dictify.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
@dataclass
class Handlers:
    """
    Processing handlers for different conversion stages.

    Handlers:
        expand: Recursive convertor for expanding the topmost level of the object's tree.
                Processing chain: skip_types → raw()/terminal() → type_handlers → obj.to_dict() → expand()
        inject_meta: Inject serialization metadata into a processed object;
                     to be called within expand() and after to_dict() processing
        raw: Custom handler for raw processing mode (max_depth < 0).
             Fallback chain: raw() → obj.to_dict() → identity function
        terminal: Custom handler for terminal processing (max_depth = 0).
                  Fallback chain: terminal() → type_handlers → obj.to_dict() → identity

    """

    expand: Callable[[Any, "DictifyOptions"], Any] = None
    inject_meta: Callable[[Any, "Meta", "DictifyOptions"], Any] = None
    raw: Callable[[Any, "DictifyOptions"], Any] = None
    terminal: Callable[[Any, "DictifyOptions"], Any] = None

    def __post_init__(self):
        self.expand = self.expand or expand
        self.inject_meta = self.inject_meta or inject_meta

HookMode

Bases: str, Enum

Object conversion strategy
  • "dict": try object's to_dict() method, then fallback
  • "dict_strict": require to_dict() method
  • "none": skip object hooks
Source code in c108/dictify.py
204
205
206
207
208
209
210
211
212
213
214
215
@unique
class HookMode(str, Enum):
    """
    Object conversion strategy:
        - "dict": try object's to_dict() method, then fallback
        - "dict_strict": require to_dict() method
        - "none": skip object hooks
    """

    DICT = "dict"
    DICT_STRICT = "dict_strict"
    NONE = "none"

Meta dataclass

Bases: MetaMixin

Comprehensive metadata for dictify conversion operations.

Contains information about trimming, sizing, and type conversion that occurred during object-to-dictionary conversion. Used internally by dictify_core() to inject metadata into processed collections and objects.

Attributes:

Name Type Description
id int | None

Object identity for reference tracking

size SizeMeta | None

Size metadata (shallow bytes, deep bytes, length)

trim TrimMeta | None

Collection trimming stats

type TypeMeta | None

Type conversion metadata

Source code in c108/dictify.py
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
@dataclass(frozen=True)
class Meta(MetaMixin):
    """
    Comprehensive metadata for dictify conversion operations.

    Contains information about trimming, sizing, and type conversion that
    occurred during object-to-dictionary conversion. Used internally by
    dictify_core() to inject metadata into processed collections and objects.

    Attributes:
        id: Object identity for reference tracking
        size: Size metadata (shallow bytes, deep bytes, length)
        trim: Collection trimming stats
        type: Type conversion metadata
    """

    VERSION: ClassVar[int] = 1  # Metadata schema version

    id: int | None = None
    size: SizeMeta | None = None
    trim: TrimMeta | None = None
    type: TypeMeta | None = None

    @classmethod
    def from_object(cls, obj: Any, *, opts: "DictifyOptions" = None) -> Self | None:
        """
        Create metadata object for dictify processing operations.

        Analyzes the source object to create metadata with size information
        and type. The Metadata creation is controlled by the flags in opts.meta configuration.

        Args:
            obj: The original object before any processing or trimming operations.
            opts: DictifyOptions instance containing metadata generation flags.

        Returns:
            Meta object containing requested metadata, or None if no metadata requested.
        """
        opts = _dictify_opts(opts)

        size_meta = SizeMeta.from_object(
            obj,
            include_len=opts.meta.len,
            include_deep=opts.meta.deep_size,
            include_shallow=opts.meta.size,
        )
        type_meta = TypeMeta.from_object(obj) if opts.meta.type else None
        id_ = id(obj) if opts.meta.id else None

        if any([size_meta, type_meta, id_]):
            return cls(size=size_meta, type=type_meta, id=id_)

        return None

    @classmethod
    def from_objects(
        cls, obj: Any, processed_obj: Any, *, opts: "DictifyOptions" = None
    ) -> Self | None:
        """
        Create metadata object for dictify processing operations.

        Analyzes the original and processed objects to generate comprehensive metadata
        including size information, trimming statistics, and type conversion details.
        Metadata creation is controlled by the flags in opts.meta configuration.

        Args:
            obj: The original object before any processing or trimming operations.
            processed_obj: The object after trimming, type conversion, or other processing.
            opts: DictifyOptions instance containing metadata generation flags and limits.

        Returns:
            Meta object containing requested metadata, or None if no metadata
            was requested or could be generated.
        """
        opts = _dictify_opts(opts)
        if not isinstance(opts, DictifyOptions):
            raise TypeError(f"opts must be a DictifyOptions instance, got {fmt_type(opts)}")
        size_meta = SizeMeta.from_object(
            obj,
            include_len=opts.meta.len,
            include_deep=opts.meta.deep_size,
            include_shallow=opts.meta.size,
        )
        trim_meta = TrimMeta.from_objects(obj, processed_obj) if opts.meta.trim else None
        type_meta = TypeMeta.from_objects(obj, processed_obj) if opts.meta.type else None

        id_ = id(obj) if opts.meta.id else None

        if any([size_meta, trim_meta, type_meta, id_]):
            return cls(size=size_meta, trim=trim_meta, type=type_meta, id=id_)

        return None

    @property
    def has_any_meta(self) -> bool:
        """Check if any metadata is present."""
        return any([self.size, self.trim, self.type])

    @property
    def is_trimmed(self) -> bool | None:
        """Check if the metadata represents a trimmed collection."""
        if self.trim is None:
            return None  # No trim metadata available
        return self.trim.is_trimmed

    def to_dict(
        self,
        include_none_attrs: bool = False,
        include_properties: bool = False,
        sort_keys: bool = False,
    ) -> dict[str, Any]:
        """
        Convert Meta info to dictionary representation.

        If no meta attrs assigned, returns a dict containing meta schema version only.
        """

        # MetaMixin.to_dict() does not do recursive expansion
        # with parameters propagation, so we do it manually
        dict_ = MetaMixin.to_dict(
            self,
            include_none_attrs=include_none_attrs,
            include_properties=include_properties,
            sort_keys=sort_keys,
        )

        dict_expanded = dict()
        for k, v in dict_.items():
            if isinstance(v, (SizeMeta, TrimMeta, TypeMeta)):
                dict_expanded[k] = v.to_dict(
                    include_none_attrs=include_none_attrs,
                    include_properties=include_properties,
                    sort_keys=sort_keys,
                )
            else:
                dict_expanded[k] = v

        dict_ = dict_expanded

        if sort_keys:
            dict_ = dict(sorted(dict_.items()))
        return dict_

has_any_meta property

Check if any metadata is present.

is_trimmed property

Check if the metadata represents a trimmed collection.

from_object(obj, *, opts=None) classmethod

Create metadata object for dictify processing operations.

Analyzes the source object to create metadata with size information and type. The Metadata creation is controlled by the flags in opts.meta configuration.

Parameters:

Name Type Description Default
obj Any

The original object before any processing or trimming operations.

required
opts DictifyOptions

DictifyOptions instance containing metadata generation flags.

None

Returns:

Type Description
Self | None

Meta object containing requested metadata, or None if no metadata requested.

Source code in c108/dictify.py
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
@classmethod
def from_object(cls, obj: Any, *, opts: "DictifyOptions" = None) -> Self | None:
    """
    Create metadata object for dictify processing operations.

    Analyzes the source object to create metadata with size information
    and type. The Metadata creation is controlled by the flags in opts.meta configuration.

    Args:
        obj: The original object before any processing or trimming operations.
        opts: DictifyOptions instance containing metadata generation flags.

    Returns:
        Meta object containing requested metadata, or None if no metadata requested.
    """
    opts = _dictify_opts(opts)

    size_meta = SizeMeta.from_object(
        obj,
        include_len=opts.meta.len,
        include_deep=opts.meta.deep_size,
        include_shallow=opts.meta.size,
    )
    type_meta = TypeMeta.from_object(obj) if opts.meta.type else None
    id_ = id(obj) if opts.meta.id else None

    if any([size_meta, type_meta, id_]):
        return cls(size=size_meta, type=type_meta, id=id_)

    return None

from_objects(obj, processed_obj, *, opts=None) classmethod

Create metadata object for dictify processing operations.

Analyzes the original and processed objects to generate comprehensive metadata including size information, trimming statistics, and type conversion details. Metadata creation is controlled by the flags in opts.meta configuration.

Parameters:

Name Type Description Default
obj Any

The original object before any processing or trimming operations.

required
processed_obj Any

The object after trimming, type conversion, or other processing.

required
opts DictifyOptions

DictifyOptions instance containing metadata generation flags and limits.

None

Returns:

Type Description
Self | None

Meta object containing requested metadata, or None if no metadata

Self | None

was requested or could be generated.

Source code in c108/dictify.py
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
@classmethod
def from_objects(
    cls, obj: Any, processed_obj: Any, *, opts: "DictifyOptions" = None
) -> Self | None:
    """
    Create metadata object for dictify processing operations.

    Analyzes the original and processed objects to generate comprehensive metadata
    including size information, trimming statistics, and type conversion details.
    Metadata creation is controlled by the flags in opts.meta configuration.

    Args:
        obj: The original object before any processing or trimming operations.
        processed_obj: The object after trimming, type conversion, or other processing.
        opts: DictifyOptions instance containing metadata generation flags and limits.

    Returns:
        Meta object containing requested metadata, or None if no metadata
        was requested or could be generated.
    """
    opts = _dictify_opts(opts)
    if not isinstance(opts, DictifyOptions):
        raise TypeError(f"opts must be a DictifyOptions instance, got {fmt_type(opts)}")
    size_meta = SizeMeta.from_object(
        obj,
        include_len=opts.meta.len,
        include_deep=opts.meta.deep_size,
        include_shallow=opts.meta.size,
    )
    trim_meta = TrimMeta.from_objects(obj, processed_obj) if opts.meta.trim else None
    type_meta = TypeMeta.from_objects(obj, processed_obj) if opts.meta.type else None

    id_ = id(obj) if opts.meta.id else None

    if any([size_meta, trim_meta, type_meta, id_]):
        return cls(size=size_meta, trim=trim_meta, type=type_meta, id=id_)

    return None

to_dict(include_none_attrs=False, include_properties=False, sort_keys=False)

Convert Meta info to dictionary representation.

If no meta attrs assigned, returns a dict containing meta schema version only.

Source code in c108/dictify.py
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
def to_dict(
    self,
    include_none_attrs: bool = False,
    include_properties: bool = False,
    sort_keys: bool = False,
) -> dict[str, Any]:
    """
    Convert Meta info to dictionary representation.

    If no meta attrs assigned, returns a dict containing meta schema version only.
    """

    # MetaMixin.to_dict() does not do recursive expansion
    # with parameters propagation, so we do it manually
    dict_ = MetaMixin.to_dict(
        self,
        include_none_attrs=include_none_attrs,
        include_properties=include_properties,
        sort_keys=sort_keys,
    )

    dict_expanded = dict()
    for k, v in dict_.items():
        if isinstance(v, (SizeMeta, TrimMeta, TypeMeta)):
            dict_expanded[k] = v.to_dict(
                include_none_attrs=include_none_attrs,
                include_properties=include_properties,
                sort_keys=sort_keys,
            )
        else:
            dict_expanded[k] = v

    dict_ = dict_expanded

    if sort_keys:
        dict_ = dict(sorted(dict_.items()))
    return dict_

MetaMixin

A mixin for Meta-data dataclasses to provide to_dict method.

Source code in c108/dictify.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class MetaMixin:
    """
    A mixin for Meta-data dataclasses to provide `to_dict` method.
    """

    def to_dict(
        self,
        include_class_vars: bool = True,
        include_none_attrs: bool = False,
        include_properties: bool = False,
        sort_keys: bool = False,
    ) -> dict[str, Any]:
        """Convert instance to a dictionary representation.

        The resulting dictionary includes all dataclass fields, class variables,
        and the values of any public properties.

        Args:
            include_class_vars: If True, class variables are included.
            include_none_attrs: If True, keys with None values are included.
            include_properties: If True, public properties are included.
            sort_keys: If True, the dictionary keys are sorted alphabetically.

        Returns:
            A dictionary representation of the instance.

        Raises:
            TypeError: If an instance class is not a dataclass.
        """
        if not is_dataclass(self):
            raise TypeError(f"{self.__class__.__name__} must be a dataclass to use MetaMixin.")

        # Mimic Shallow asdict()
        dict_ = {f.name: getattr(self, f.name) for f in fields(self)}

        # Add class variables if requested
        if include_class_vars:
            dict_.update(self._get_class_vars())

        # Add public properties if requested
        if include_properties:
            dict_.update(self._get_public_properties())

        # Filter out None values if requested
        if not include_none_attrs:
            dict_ = {k: v for k, v in dict_.items() if v is not None}

        # Sort keys if requested
        if sort_keys:
            dict_ = dict(sorted(dict_.items()))

        return dict_

    def _get_public_properties(self) -> dict[str, Any]:
        """Inspect the instance and return a dict of its public property values."""
        properties = {}
        # Iterate through the entire MRO to get properties from parent classes too
        for cls in self.__class__.__mro__:
            for name in dir(cls):
                if name.startswith("_"):
                    continue

                # Check if the attribute is a property on the class
                attr = getattr(cls, name, None)
                if isinstance(attr, property) and name not in properties:
                    properties[name] = getattr(self, name)
        return properties

    def _get_class_vars(self) -> dict[str, Any]:
        """Get class variables (non-dataclass fields) from the instance's class."""
        class_vars = {}
        # Get all dataclass field names to exclude them
        if is_dataclass(self):
            field_names = {f.name for f in fields(self)}
        else:
            field_names = set()

        # Iterate through the MRO to get class vars from parent classes too
        for cls in self.__class__.__mro__:
            if cls is object or cls is MetaMixin:
                continue

            for name, value in cls.__dict__.items():
                # Skip private attributes, methods, properties, and dataclass fields
                if (
                    name.startswith("_")
                    or callable(value)
                    or isinstance(value, (property, staticmethod, classmethod))
                    or name in field_names
                    or name in class_vars  # Already found in a child class
                ):
                    continue

                class_vars[name] = value

        return class_vars

to_dict(include_class_vars=True, include_none_attrs=False, include_properties=False, sort_keys=False)

Convert instance to a dictionary representation.

The resulting dictionary includes all dataclass fields, class variables, and the values of any public properties.

Parameters:

Name Type Description Default
include_class_vars bool

If True, class variables are included.

True
include_none_attrs bool

If True, keys with None values are included.

False
include_properties bool

If True, public properties are included.

False
sort_keys bool

If True, the dictionary keys are sorted alphabetically.

False

Returns:

Type Description
dict[str, Any]

A dictionary representation of the instance.

Raises:

Type Description
TypeError

If an instance class is not a dataclass.

Source code in c108/dictify.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def to_dict(
    self,
    include_class_vars: bool = True,
    include_none_attrs: bool = False,
    include_properties: bool = False,
    sort_keys: bool = False,
) -> dict[str, Any]:
    """Convert instance to a dictionary representation.

    The resulting dictionary includes all dataclass fields, class variables,
    and the values of any public properties.

    Args:
        include_class_vars: If True, class variables are included.
        include_none_attrs: If True, keys with None values are included.
        include_properties: If True, public properties are included.
        sort_keys: If True, the dictionary keys are sorted alphabetically.

    Returns:
        A dictionary representation of the instance.

    Raises:
        TypeError: If an instance class is not a dataclass.
    """
    if not is_dataclass(self):
        raise TypeError(f"{self.__class__.__name__} must be a dataclass to use MetaMixin.")

    # Mimic Shallow asdict()
    dict_ = {f.name: getattr(self, f.name) for f in fields(self)}

    # Add class variables if requested
    if include_class_vars:
        dict_.update(self._get_class_vars())

    # Add public properties if requested
    if include_properties:
        dict_.update(self._get_public_properties())

    # Filter out None values if requested
    if not include_none_attrs:
        dict_ = {k: v for k, v in dict_.items() if v is not None}

    # Sort keys if requested
    if sort_keys:
        dict_ = dict(sorted(dict_.items()))

    return dict_

MetaOptions dataclass

Metadata generation and injection options for dictify operations.

Controls what metadata gets injected into converted objects, including size information, trimming statistics, and type conversion details. Metadata is injected either as a dictionary key (for mappings) or appended as the final element (for sequences/sets).

Attributes:

Name Type Description
in_expand bool

Include metadata in object expansion (during attribute extraction)

in_to_dict bool

Inject metadata into to_dict() method results

id bool

Add id to metadata on first obj occurrence (for cylyc references tracking)

key str

Dictionary key used for metadata injection in mappings (default: "dictify")

len bool

Include collection length in size metadata

size bool

Include shallow object size in bytes (via sys.getsizeof)

deep_size bool

Include deep object size calculation (expensive operation)

trim bool

Inject trimming statistics when collections exceed max_items limit

type bool

Include type conversion metadata when object types change during processing

Examples:

>>> # Enable all metadata
>>> meta = MetaOptions(len=True, size=True, deep_size=True, type=True)
>>> # Only trimming metadata (default)
>>> meta = MetaOptions()  # trim=True by default
>>> # Custom metadata key
>>> meta = MetaOptions(key="__meta", trim=True, type=True)
Source code in c108/dictify.py
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
@dataclass
class MetaOptions:
    """
    Metadata generation and injection options for dictify operations.

    Controls what metadata gets injected into converted objects, including size information,
    trimming statistics, and type conversion details. Metadata is injected either as a
    dictionary key (for mappings) or appended as the final element (for sequences/sets).

    Attributes:
        in_expand: Include metadata in object expansion (during attribute extraction)
        in_to_dict: Inject metadata into to_dict() method results
        id: Add __id__ to metadata on first obj occurrence (for cylyc references tracking)
        key: Dictionary key used for metadata injection in mappings (default: "__dictify__")
        len: Include collection length in size metadata
        size: Include shallow object size in bytes (via sys.getsizeof)
        deep_size: Include deep object size calculation (expensive operation)
        trim: Inject trimming statistics when collections exceed max_items limit
        type: Include type conversion metadata when object types change during processing

    Examples:
        >>> # Enable all metadata
        >>> meta = MetaOptions(len=True, size=True, deep_size=True, type=True)

        >>> # Only trimming metadata (default)
        >>> meta = MetaOptions()  # trim=True by default

        >>> # Custom metadata key
        >>> meta = MetaOptions(key="__meta", trim=True, type=True)
    """

    # Injection into object processor's output
    in_expand: bool = False
    in_to_dict: bool = False

    # Reference tracking
    id: bool = False

    # Injection Key
    key: str = "__dictify__"

    # Size-related metadata
    len: bool = False
    size: bool = False  # Shallow size
    deep_size: bool = False  # Deep size (expensive)

    # Operation metadata
    trim: bool = False  # Trimming statistics
    type: bool = False  # Type conversion info

    @property
    def any_enabled(self) -> bool:
        """Check if any metadata injection is enabled."""
        return any([self.sizes_enabled, self.trim, self.type])

    @property
    def sizes_enabled(self) -> bool:
        """Check if any size-related metadata injection is enabled."""
        return any([self.len, self.size, self.deep_size])

    def merge(
        self,
        *,
        in_expand: bool = UNSET,
        in_to_dict: bool = UNSET,
        key: str = UNSET,
        len: bool = UNSET,
        size: bool = UNSET,
        deep_size: bool = UNSET,
        trim: bool = UNSET,
        type: bool = UNSET,
        inject_trim_meta: bool = UNSET,
        inject_type_meta: bool = UNSET,
    ) -> Self:
        """Create a new instance with merged configuration options.

        Use either the convenience parameter or the associated explicit attributes, but not both in the same call.
        Unspecified parameters retain their original values.

        Convenience parameters:
          - inject_trim_meta:
              - True  -> sets trim=True and forces in_expand=True and in_to_dict=True
              - False -> sets trim=False
              - Cannot be used together with: trim, in_expand, or in_to_dict
          - inject_type_meta:
              - True  -> sets type=True and forces in_expand=True and in_to_dict=True
              - False -> sets type=False
              - Cannot be used together with: type, in_expand, or in_to_dict

        Args:
            in_expand: Include metadata in object expansion (attribute extraction).
            in_to_dict: Inject metadata into to_dict() method results.
            key: Dictionary key for metadata injection in mappings.
            len: Include collection length in size metadata.
            size: Include shallow object size in bytes.
            deep_size: Include deep object size calculation.
            trim: Inject trimming statistics when collections exceed limits.
            type: Include type conversion metadata when object types change.
            inject_trim_meta: Convenience flag to toggle trimming metadata and ensure injection points.
                             Mutually exclusive with trim, in_expand, and in_to_dict.
            inject_type_meta: Convenience flag to toggle type metadata and ensure injection points.
                             Mutually exclusive with type, in_expand, and in_to_dict.

        Returns:
            MetaOptions: New MetaOptions instance with merged configuration.

        Raises:
            ValueError: If inject_trim_meta is used with trim, in_expand, or in_to_dict.
            ValueError: If inject_type_meta is used with type, in_expand, or in_to_dict.
            TypeError: If inject_trim_meta or inject_type_meta is not a bool.
        """
        # Check mutual exclusivity for inject_trim_meta
        if inject_trim_meta is not UNSET:
            if trim is not UNSET:
                raise ValueError(
                    "inject_trim_meta cannot be used together with trim. "
                    "Use either the convenience flag or the explicit parameter, not both."
                )
            if in_expand is not UNSET:
                raise ValueError(
                    "inject_trim_meta cannot be used together with in_expand. "
                    "Use either the convenience flag or the explicit parameter, not both."
                )
            if in_to_dict is not UNSET:
                raise ValueError(
                    "inject_trim_meta cannot be used together with in_to_dict. "
                    "Use either the convenience flag or the explicit parameter, not both."
                )

        # Check mutual exclusivity for inject_type_meta
        if inject_type_meta is not UNSET:
            if type is not UNSET:
                raise ValueError(
                    "inject_type_meta cannot be used together with type. "
                    "Use either the convenience flag or the explicit parameter, not both."
                )
            if in_expand is not UNSET:
                raise ValueError(
                    "inject_type_meta cannot be used together with in_expand. "
                    "Use either the convenience flag or the explicit parameter, not both."
                )
            if in_to_dict is not UNSET:
                raise ValueError(
                    "inject_type_meta cannot be used together with in_to_dict. "
                    "Use either the convenience flag or the explicit parameter, not both."
                )

        # Start from current values
        new_in_expand = self.in_expand
        new_in_to_dict = self.in_to_dict
        new_trim = self.trim
        new_type = self.type

        # Apply convenience flags
        if inject_trim_meta is not UNSET:
            inject_trim_meta = bool(inject_trim_meta)
            if inject_trim_meta:
                new_trim = True
                new_in_expand = True
                new_in_to_dict = True
            else:
                new_trim = False

        if inject_type_meta is not UNSET:
            inject_type_meta = bool(inject_type_meta)
            if inject_type_meta:
                new_type = True
                new_in_expand = True
                new_in_to_dict = True
            else:
                new_type = False

        # Apply explicit args (only if convenience flags weren't used - already validated)
        new_trim = ifnotunset(trim, default=new_trim)
        new_type = ifnotunset(type, default=new_type)
        new_in_expand = ifnotunset(in_expand, default=new_in_expand)
        new_in_to_dict = ifnotunset(in_to_dict, default=new_in_to_dict)

        # Merge remaining, non-convenience fields as usual
        new_key = _merge_new_default(key, self.key)
        new_len = _merge_new_default(len, self.len)
        new_size = _merge_new_default(size, self.size)
        new_deep_size = _merge_new_default(deep_size, self.deep_size)

        return self.__class__(
            in_expand=new_in_expand,
            in_to_dict=new_in_to_dict,
            key=new_key,
            len=new_len,
            size=new_size,
            deep_size=new_deep_size,
            trim=new_trim,
            type=new_type,
        )

any_enabled property

Check if any metadata injection is enabled.

sizes_enabled property

Check if any size-related metadata injection is enabled.

merge(*, in_expand=UNSET, in_to_dict=UNSET, key=UNSET, len=UNSET, size=UNSET, deep_size=UNSET, trim=UNSET, type=UNSET, inject_trim_meta=UNSET, inject_type_meta=UNSET)

Create a new instance with merged configuration options.

Use either the convenience parameter or the associated explicit attributes, but not both in the same call. Unspecified parameters retain their original values.

Convenience parameters
  • inject_trim_meta:
    • True -> sets trim=True and forces in_expand=True and in_to_dict=True
    • False -> sets trim=False
    • Cannot be used together with: trim, in_expand, or in_to_dict
  • inject_type_meta:
    • True -> sets type=True and forces in_expand=True and in_to_dict=True
    • False -> sets type=False
    • Cannot be used together with: type, in_expand, or in_to_dict

Parameters:

Name Type Description Default
in_expand bool

Include metadata in object expansion (attribute extraction).

UNSET
in_to_dict bool

Inject metadata into to_dict() method results.

UNSET
key str

Dictionary key for metadata injection in mappings.

UNSET
len bool

Include collection length in size metadata.

UNSET
size bool

Include shallow object size in bytes.

UNSET
deep_size bool

Include deep object size calculation.

UNSET
trim bool

Inject trimming statistics when collections exceed limits.

UNSET
type bool

Include type conversion metadata when object types change.

UNSET
inject_trim_meta bool

Convenience flag to toggle trimming metadata and ensure injection points. Mutually exclusive with trim, in_expand, and in_to_dict.

UNSET
inject_type_meta bool

Convenience flag to toggle type metadata and ensure injection points. Mutually exclusive with type, in_expand, and in_to_dict.

UNSET

Returns:

Name Type Description
MetaOptions Self

New MetaOptions instance with merged configuration.

Raises:

Type Description
ValueError

If inject_trim_meta is used with trim, in_expand, or in_to_dict.

ValueError

If inject_type_meta is used with type, in_expand, or in_to_dict.

TypeError

If inject_trim_meta or inject_type_meta is not a bool.

Source code in c108/dictify.py
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
def merge(
    self,
    *,
    in_expand: bool = UNSET,
    in_to_dict: bool = UNSET,
    key: str = UNSET,
    len: bool = UNSET,
    size: bool = UNSET,
    deep_size: bool = UNSET,
    trim: bool = UNSET,
    type: bool = UNSET,
    inject_trim_meta: bool = UNSET,
    inject_type_meta: bool = UNSET,
) -> Self:
    """Create a new instance with merged configuration options.

    Use either the convenience parameter or the associated explicit attributes, but not both in the same call.
    Unspecified parameters retain their original values.

    Convenience parameters:
      - inject_trim_meta:
          - True  -> sets trim=True and forces in_expand=True and in_to_dict=True
          - False -> sets trim=False
          - Cannot be used together with: trim, in_expand, or in_to_dict
      - inject_type_meta:
          - True  -> sets type=True and forces in_expand=True and in_to_dict=True
          - False -> sets type=False
          - Cannot be used together with: type, in_expand, or in_to_dict

    Args:
        in_expand: Include metadata in object expansion (attribute extraction).
        in_to_dict: Inject metadata into to_dict() method results.
        key: Dictionary key for metadata injection in mappings.
        len: Include collection length in size metadata.
        size: Include shallow object size in bytes.
        deep_size: Include deep object size calculation.
        trim: Inject trimming statistics when collections exceed limits.
        type: Include type conversion metadata when object types change.
        inject_trim_meta: Convenience flag to toggle trimming metadata and ensure injection points.
                         Mutually exclusive with trim, in_expand, and in_to_dict.
        inject_type_meta: Convenience flag to toggle type metadata and ensure injection points.
                         Mutually exclusive with type, in_expand, and in_to_dict.

    Returns:
        MetaOptions: New MetaOptions instance with merged configuration.

    Raises:
        ValueError: If inject_trim_meta is used with trim, in_expand, or in_to_dict.
        ValueError: If inject_type_meta is used with type, in_expand, or in_to_dict.
        TypeError: If inject_trim_meta or inject_type_meta is not a bool.
    """
    # Check mutual exclusivity for inject_trim_meta
    if inject_trim_meta is not UNSET:
        if trim is not UNSET:
            raise ValueError(
                "inject_trim_meta cannot be used together with trim. "
                "Use either the convenience flag or the explicit parameter, not both."
            )
        if in_expand is not UNSET:
            raise ValueError(
                "inject_trim_meta cannot be used together with in_expand. "
                "Use either the convenience flag or the explicit parameter, not both."
            )
        if in_to_dict is not UNSET:
            raise ValueError(
                "inject_trim_meta cannot be used together with in_to_dict. "
                "Use either the convenience flag or the explicit parameter, not both."
            )

    # Check mutual exclusivity for inject_type_meta
    if inject_type_meta is not UNSET:
        if type is not UNSET:
            raise ValueError(
                "inject_type_meta cannot be used together with type. "
                "Use either the convenience flag or the explicit parameter, not both."
            )
        if in_expand is not UNSET:
            raise ValueError(
                "inject_type_meta cannot be used together with in_expand. "
                "Use either the convenience flag or the explicit parameter, not both."
            )
        if in_to_dict is not UNSET:
            raise ValueError(
                "inject_type_meta cannot be used together with in_to_dict. "
                "Use either the convenience flag or the explicit parameter, not both."
            )

    # Start from current values
    new_in_expand = self.in_expand
    new_in_to_dict = self.in_to_dict
    new_trim = self.trim
    new_type = self.type

    # Apply convenience flags
    if inject_trim_meta is not UNSET:
        inject_trim_meta = bool(inject_trim_meta)
        if inject_trim_meta:
            new_trim = True
            new_in_expand = True
            new_in_to_dict = True
        else:
            new_trim = False

    if inject_type_meta is not UNSET:
        inject_type_meta = bool(inject_type_meta)
        if inject_type_meta:
            new_type = True
            new_in_expand = True
            new_in_to_dict = True
        else:
            new_type = False

    # Apply explicit args (only if convenience flags weren't used - already validated)
    new_trim = ifnotunset(trim, default=new_trim)
    new_type = ifnotunset(type, default=new_type)
    new_in_expand = ifnotunset(in_expand, default=new_in_expand)
    new_in_to_dict = ifnotunset(in_to_dict, default=new_in_to_dict)

    # Merge remaining, non-convenience fields as usual
    new_key = _merge_new_default(key, self.key)
    new_len = _merge_new_default(len, self.len)
    new_size = _merge_new_default(size, self.size)
    new_deep_size = _merge_new_default(deep_size, self.deep_size)

    return self.__class__(
        in_expand=new_in_expand,
        in_to_dict=new_in_to_dict,
        key=new_key,
        len=new_len,
        size=new_size,
        deep_size=new_deep_size,
        trim=new_trim,
        type=new_type,
    )

SizeMeta dataclass

Bases: MetaMixin

Metadata about object size information.

Attributes:

Name Type Description
len int | None

Object's len if defined.

deep int | None

Deep size in bytes.

shallow int | None

Shallow size in bytes of the source object (e.g., sys.getsizeof(obj)).

Source code in c108/dictify.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
@dataclass(frozen=True)
class SizeMeta(MetaMixin):
    """Metadata about object size information.

    Attributes:
        len: Object's __len__ if defined.
        deep: Deep size in bytes.
        shallow: Shallow size in bytes of the source object (e.g., sys.getsizeof(obj)).
    """

    len: int | None = None
    deep: int | None = None
    shallow: int | None = None

    def __post_init__(self) -> None:
        """Validate constraints."""
        if all(val is None for val in (self.len, self.deep, self.shallow)):
            raise ValueError("SizeMeta requires at least one non-None value")

        if self.deep is not None and self.shallow is not None and self.deep < self.shallow:
            raise ValueError("SizeMeta.deep >= SizeMeta.shallow expected")

    @classmethod
    def from_object(
        cls,
        obj: Any,
        *,
        include_len: bool = False,
        include_deep: bool = False,
        include_shallow: bool = False,
    ) -> Self | None:
        """
        Create SizeMeta instance from an object with specified size measurements.

        Args:
            obj: The object to measure.
            include_len: If True, include the object's length (if it has __len__).
            include_deep: If True, include deep size in bytes using deep_sizeof().
            include_shallow: If True, include shallow size in bytes using sys.getsizeof().

        Returns:
            SizeMeta instance with requested measurements, or None if no measurements
            were requested or if all requested measurements failed to produce values.

        Note:
            At least one include_* parameter must be True to get a non-None result.
            Length is only included for objects that support __len__.
        """
        if not any([include_len, include_deep, include_shallow]):
            return None

        len_ = _length_or_none(obj) if include_len else None

        try:
            deep_ = deep_sizeof(obj) if include_deep else None
        except Exception:
            deep_ = None

        try:
            shallow_ = sys.getsizeof(obj) if include_shallow else None
        except Exception:
            shallow_ = None

        if all(val is None for val in (len_, deep_, shallow_)):
            return None

        return cls(len=len_, deep=deep_, shallow=shallow_)

__post_init__()

Validate constraints.

Source code in c108/dictify.py
330
331
332
333
334
335
336
def __post_init__(self) -> None:
    """Validate constraints."""
    if all(val is None for val in (self.len, self.deep, self.shallow)):
        raise ValueError("SizeMeta requires at least one non-None value")

    if self.deep is not None and self.shallow is not None and self.deep < self.shallow:
        raise ValueError("SizeMeta.deep >= SizeMeta.shallow expected")

from_object(obj, *, include_len=False, include_deep=False, include_shallow=False) classmethod

Create SizeMeta instance from an object with specified size measurements.

Parameters:

Name Type Description Default
obj Any

The object to measure.

required
include_len bool

If True, include the object's length (if it has len).

False
include_deep bool

If True, include deep size in bytes using deep_sizeof().

False
include_shallow bool

If True, include shallow size in bytes using sys.getsizeof().

False

Returns:

Type Description
Self | None

SizeMeta instance with requested measurements, or None if no measurements

Self | None

were requested or if all requested measurements failed to produce values.

Note

At least one include_* parameter must be True to get a non-None result. Length is only included for objects that support len.

Source code in c108/dictify.py
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
@classmethod
def from_object(
    cls,
    obj: Any,
    *,
    include_len: bool = False,
    include_deep: bool = False,
    include_shallow: bool = False,
) -> Self | None:
    """
    Create SizeMeta instance from an object with specified size measurements.

    Args:
        obj: The object to measure.
        include_len: If True, include the object's length (if it has __len__).
        include_deep: If True, include deep size in bytes using deep_sizeof().
        include_shallow: If True, include shallow size in bytes using sys.getsizeof().

    Returns:
        SizeMeta instance with requested measurements, or None if no measurements
        were requested or if all requested measurements failed to produce values.

    Note:
        At least one include_* parameter must be True to get a non-None result.
        Length is only included for objects that support __len__.
    """
    if not any([include_len, include_deep, include_shallow]):
        return None

    len_ = _length_or_none(obj) if include_len else None

    try:
        deep_ = deep_sizeof(obj) if include_deep else None
    except Exception:
        deep_ = None

    try:
        shallow_ = sys.getsizeof(obj) if include_shallow else None
    except Exception:
        shallow_ = None

    if all(val is None for val in (len_, deep_, shallow_)):
        return None

    return cls(len=len_, deep=deep_, shallow=shallow_)

TrimMeta dataclass

Bases: MetaMixin

Metadata about collection trimming operations.

Supports both sized collections and iterables of unknown length (generators, etc).

Attributes:

Name Type Description
len int | None

Total number of elements in original iterable, or None if unknown.

shown int | None

Number of elements kept/shown after trimming.

is_trimmed bool | None

(property) Whether trimming occurred. None if len is unknown.

trimmed int | None

(property) Number of elements removed. None if len is unknown.

Source code in c108/dictify.py
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
@dataclass(frozen=True)
class TrimMeta(MetaMixin):
    """Metadata about collection trimming operations.

    Supports both sized collections and iterables of unknown length (generators, etc).

    Attributes:
        len: Total number of elements in original iterable, or None if unknown.
        shown: Number of elements kept/shown after trimming.
        is_trimmed: (property) Whether trimming occurred. None if len is unknown.
        trimmed: (property) Number of elements removed. None if len is unknown.
    """

    len: int | None = None
    shown: int | None = None

    def __post_init__(self) -> None:
        """Validate business logic constraints."""
        if self.shown is None:
            raise ValueError("TrimMeta requires 'shown' attribute.")

        # Only business logic validation
        if self.len is not None and self.shown is not None and self.shown > self.len:
            raise ValueError("TrimMeta.shown <= TrimMeta.len expected")

    @classmethod
    def from_objects(cls, obj: Any, processed_object: Any) -> Self | None:
        """
        Create TrimMeta instance by comparing original and processed objects.

        This returns None in cases where a size comparison would be misleading:
        - If processed_object is mapping/items-iterable but obj is not (non-mapping -> mapping).
        - If processed_object is iterable but obj is not (non-iterable -> iterable).
        - If processed_object has unknown length.

        Otherwise, len reflects the size of obj (or None if unknown), and shown reflects
        the size of processed_object.

        Args:
            obj: The original object.
            processed_object: The processed/trimmed object.

        Returns:
            TrimMeta instance with length comparison data, or None
            if comparison is not meaningfull or processed_object has unknown length.

        Examples:
            Normal comparison with both sizes known:
                >>> TrimMeta.from_objects([1, 2, 3, 4, 5], [1, 2, 3])
                TrimMeta(len=5, shown=3)

            Original size unknown, processed size known:
                >>> orig = (i for i in range(10))  # generator, unknown length
                >>> TrimMeta.from_objects(orig, [0, 1, 2])
                TrimMeta(len=None, shown=3)

            Processed size unknown (generator) -> returns None:
                >>> TrimMeta.from_objects([1, 2, 3, 4], (i for i in range(2)))


            Converted non-iterable to iterable -> returns None:
                >>> TrimMeta.from_objects(42, [42])

            Converted non-mapping to mapping/items-iterable -> returns None:
                >>> TrimMeta.from_objects({1, 2, 3}, {"a": 1, "b": 2})
        """

        if not _is_items_iterable(obj) and _is_items_iterable(processed_object):
            # If we've converted non-mapping to mapping, return None
            return None

        if not _is_iterable(obj) and _is_iterable(processed_object):
            # If we've converted non-iterable to iterable, return None
            return None

        original_len = _length_or_none(obj)
        processed_len = _length_or_none(processed_object)

        if processed_len is None:
            # Can't create metadata without knowing what we're showing
            return None

        return cls(len=original_len, shown=processed_len)

    @classmethod
    def from_trimmed(cls, total_len: int, trimmed_len: int) -> Self:
        """Create TrimMeta from total length and trimmed items count.

        Args:
            total_len: Total number of elements in the original collection.
            trimmed_len: Number of elements that were trimmed.

        Returns:
            TrimMeta instance with computed shown value.
        """
        shown = max(total_len - trimmed_len, 0)
        return cls(len=total_len, shown=shown)

    @property
    def is_trimmed(self) -> bool | None:
        """Whether the collection was trimmed.

        Returns None if source length unknown (unsized iterable).
        """
        if self.len is None:
            return None
        return self.shown < self.len

    @property
    def trimmed(self) -> int | None:
        """Number of elements removed due to trimming.

        Returns None if source length unknown (unsized iterable).
        """
        if self.len is None:
            return None
        return self.len - self.shown

is_trimmed property

Whether the collection was trimmed.

Returns None if source length unknown (unsized iterable).

trimmed property

Number of elements removed due to trimming.

Returns None if source length unknown (unsized iterable).

__post_init__()

Validate business logic constraints.

Source code in c108/dictify.py
401
402
403
404
405
406
407
408
def __post_init__(self) -> None:
    """Validate business logic constraints."""
    if self.shown is None:
        raise ValueError("TrimMeta requires 'shown' attribute.")

    # Only business logic validation
    if self.len is not None and self.shown is not None and self.shown > self.len:
        raise ValueError("TrimMeta.shown <= TrimMeta.len expected")

from_objects(obj, processed_object) classmethod

Create TrimMeta instance by comparing original and processed objects.

This returns None in cases where a size comparison would be misleading: - If processed_object is mapping/items-iterable but obj is not (non-mapping -> mapping). - If processed_object is iterable but obj is not (non-iterable -> iterable). - If processed_object has unknown length.

Otherwise, len reflects the size of obj (or None if unknown), and shown reflects the size of processed_object.

Parameters:

Name Type Description Default
obj Any

The original object.

required
processed_object Any

The processed/trimmed object.

required

Returns:

Type Description
Self | None

TrimMeta instance with length comparison data, or None

Self | None

if comparison is not meaningfull or processed_object has unknown length.

Examples:

Normal comparison with both sizes known: >>> TrimMeta.from_objects([1, 2, 3, 4, 5], [1, 2, 3]) TrimMeta(len=5, shown=3)

Original size unknown, processed size known: >>> orig = (i for i in range(10)) # generator, unknown length >>> TrimMeta.from_objects(orig, [0, 1, 2]) TrimMeta(len=None, shown=3)

Processed size unknown (generator) -> returns None: >>> TrimMeta.from_objects([1, 2, 3, 4], (i for i in range(2)))

Converted non-iterable to iterable -> returns None: >>> TrimMeta.from_objects(42, [42])

Converted non-mapping to mapping/items-iterable -> returns None: >>> TrimMeta.from_objects({1, 2, 3}, {"a": 1, "b": 2})

Source code in c108/dictify.py
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
@classmethod
def from_objects(cls, obj: Any, processed_object: Any) -> Self | None:
    """
    Create TrimMeta instance by comparing original and processed objects.

    This returns None in cases where a size comparison would be misleading:
    - If processed_object is mapping/items-iterable but obj is not (non-mapping -> mapping).
    - If processed_object is iterable but obj is not (non-iterable -> iterable).
    - If processed_object has unknown length.

    Otherwise, len reflects the size of obj (or None if unknown), and shown reflects
    the size of processed_object.

    Args:
        obj: The original object.
        processed_object: The processed/trimmed object.

    Returns:
        TrimMeta instance with length comparison data, or None
        if comparison is not meaningfull or processed_object has unknown length.

    Examples:
        Normal comparison with both sizes known:
            >>> TrimMeta.from_objects([1, 2, 3, 4, 5], [1, 2, 3])
            TrimMeta(len=5, shown=3)

        Original size unknown, processed size known:
            >>> orig = (i for i in range(10))  # generator, unknown length
            >>> TrimMeta.from_objects(orig, [0, 1, 2])
            TrimMeta(len=None, shown=3)

        Processed size unknown (generator) -> returns None:
            >>> TrimMeta.from_objects([1, 2, 3, 4], (i for i in range(2)))


        Converted non-iterable to iterable -> returns None:
            >>> TrimMeta.from_objects(42, [42])

        Converted non-mapping to mapping/items-iterable -> returns None:
            >>> TrimMeta.from_objects({1, 2, 3}, {"a": 1, "b": 2})
    """

    if not _is_items_iterable(obj) and _is_items_iterable(processed_object):
        # If we've converted non-mapping to mapping, return None
        return None

    if not _is_iterable(obj) and _is_iterable(processed_object):
        # If we've converted non-iterable to iterable, return None
        return None

    original_len = _length_or_none(obj)
    processed_len = _length_or_none(processed_object)

    if processed_len is None:
        # Can't create metadata without knowing what we're showing
        return None

    return cls(len=original_len, shown=processed_len)

from_trimmed(total_len, trimmed_len) classmethod

Create TrimMeta from total length and trimmed items count.

Parameters:

Name Type Description Default
total_len int

Total number of elements in the original collection.

required
trimmed_len int

Number of elements that were trimmed.

required

Returns:

Type Description
Self

TrimMeta instance with computed shown value.

Source code in c108/dictify.py
469
470
471
472
473
474
475
476
477
478
479
480
481
@classmethod
def from_trimmed(cls, total_len: int, trimmed_len: int) -> Self:
    """Create TrimMeta from total length and trimmed items count.

    Args:
        total_len: Total number of elements in the original collection.
        trimmed_len: Number of elements that were trimmed.

    Returns:
        TrimMeta instance with computed shown value.
    """
    shown = max(total_len - trimmed_len, 0)
    return cls(len=total_len, shown=shown)

TypeMeta dataclass

Bases: MetaMixin

Metadata about type information and conversion.

Attributes:

Name Type Description
from_type type | None

Type of the original object.

to_type type | None

Type of the converted object.

Source code in c108/dictify.py
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
@dataclass(frozen=True)
class TypeMeta(MetaMixin):
    """
    Metadata about type information and conversion.

    Attributes:
        from_type: Type of the original object.
        to_type: Type of the converted object.
    """

    from_type: type | None = None
    to_type: type | None = None

    @classmethod
    def from_object(cls, obj: Any) -> Self:
        """
        Create TypeMeta instance from an object.

        Args:
            obj: The source object.

        Returns:
            TypeMeta instance with the runtime type of obj.

        Note:
            Captures actual type including NoneType for None value.
        """
        return cls(from_type=type(obj))

    @classmethod
    def from_objects(cls, obj: Any, processed_object: Any) -> Self:
        """
        Create TypeMeta instance by comparing original and processed objects.

        Args:
            obj: The original object.
            processed_object: The processed/converted object.

        Returns:
            TypeMeta instance with the runtime types of both objects.

        Note:
            Captures actual types including NoneType for None values.
        """
        from_type = type(obj)
        to_type = type(processed_object)

        return cls(from_type=from_type, to_type=to_type)

    @property
    def is_converted(self) -> bool:
        """Check if type conversion occurred."""
        if self.from_type is None or self.to_type is None:
            # Can't determine conversion without both types
            return False

        return self.from_type != self.to_type

    def to_dict(
        self,
        include_none_attrs: bool = False,
        include_properties: bool = False,
        sort_keys: bool = False,
    ) -> dict[str, Any]:
        """Convert to dictionary representation.

        The resulting dictionary includes all dataclass fields and the values
        of any public properties.

        Args:
            include_none_attrs: If True, keys with None values are included.
            include_properties: If True, public properties are included.
            sort_keys: If True, the dictionary keys are sorted alphabetically.

        Returns:
            A dictionary representation of the instance.

        Raises:
            TypeError: If the instance class is not a dataclass.
        """
        dict_ = MetaMixin.to_dict(
            self,
            include_none_attrs=include_none_attrs,
            include_properties=include_properties,
            sort_keys=sort_keys,
        )

        if not self.is_converted and not include_none_attrs:
            # When is not converted, to_type is redundant - but only remove if not including None attrs
            dict_.pop("to_type", None)

        return dict_

is_converted property

Check if type conversion occurred.

from_object(obj) classmethod

Create TypeMeta instance from an object.

Parameters:

Name Type Description Default
obj Any

The source object.

required

Returns:

Type Description
Self

TypeMeta instance with the runtime type of obj.

Note

Captures actual type including NoneType for None value.

Source code in c108/dictify.py
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
@classmethod
def from_object(cls, obj: Any) -> Self:
    """
    Create TypeMeta instance from an object.

    Args:
        obj: The source object.

    Returns:
        TypeMeta instance with the runtime type of obj.

    Note:
        Captures actual type including NoneType for None value.
    """
    return cls(from_type=type(obj))

from_objects(obj, processed_object) classmethod

Create TypeMeta instance by comparing original and processed objects.

Parameters:

Name Type Description Default
obj Any

The original object.

required
processed_object Any

The processed/converted object.

required

Returns:

Type Description
Self

TypeMeta instance with the runtime types of both objects.

Note

Captures actual types including NoneType for None values.

Source code in c108/dictify.py
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
@classmethod
def from_objects(cls, obj: Any, processed_object: Any) -> Self:
    """
    Create TypeMeta instance by comparing original and processed objects.

    Args:
        obj: The original object.
        processed_object: The processed/converted object.

    Returns:
        TypeMeta instance with the runtime types of both objects.

    Note:
        Captures actual types including NoneType for None values.
    """
    from_type = type(obj)
    to_type = type(processed_object)

    return cls(from_type=from_type, to_type=to_type)

to_dict(include_none_attrs=False, include_properties=False, sort_keys=False)

Convert to dictionary representation.

The resulting dictionary includes all dataclass fields and the values of any public properties.

Parameters:

Name Type Description Default
include_none_attrs bool

If True, keys with None values are included.

False
include_properties bool

If True, public properties are included.

False
sort_keys bool

If True, the dictionary keys are sorted alphabetically.

False

Returns:

Type Description
dict[str, Any]

A dictionary representation of the instance.

Raises:

Type Description
TypeError

If the instance class is not a dataclass.

Source code in c108/dictify.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
def to_dict(
    self,
    include_none_attrs: bool = False,
    include_properties: bool = False,
    sort_keys: bool = False,
) -> dict[str, Any]:
    """Convert to dictionary representation.

    The resulting dictionary includes all dataclass fields and the values
    of any public properties.

    Args:
        include_none_attrs: If True, keys with None values are included.
        include_properties: If True, public properties are included.
        sort_keys: If True, the dictionary keys are sorted alphabetically.

    Returns:
        A dictionary representation of the instance.

    Raises:
        TypeError: If the instance class is not a dataclass.
    """
    dict_ = MetaMixin.to_dict(
        self,
        include_none_attrs=include_none_attrs,
        include_properties=include_properties,
        sort_keys=sort_keys,
    )

    if not self.is_converted and not include_none_attrs:
        # When is not converted, to_type is redundant - but only remove if not including None attrs
        dict_.pop("to_type", None)

    return dict_

dictify(obj, *, max_depth=3, max_items=100, max_str_len=200, max_bytes=512, include_none=False, include_private=False, include_properties=False, include_class_name=False, sort_keys=False, sort_iterables=False, opts=None)

Convert Python objects to human-readable dictionaries with common customizations.

Simplified wrapper around dictify_core() for everyday use cases: CLI printing, basic logging, and object inspection. Provides direct parameter access to the most common conversion options with sensible defaults (DictifyOptions()).

For specialized presets (logging, debugging, serialization) or advanced features (custom type handlers, edge-case processing, metadata injection), use dictify_core() with DictifyOptions.

Parameters:

Name Type Description Default
obj Any

Object to convert to dictionary representation

required
Depth & Size Controls

max_depth: Maximum recursion depth for nested objects (default: 3) max_items: Maximum items in collections before trimming (default: 100). None = no limit (process entire collection). max_str_len: String truncation limit (default: 200), None = no truncation max_bytes: Bytes object truncation limit (default: 512), None = no truncation

required
Attribute Filtering

include_none: Include attributes and dictionary items with None values include_private: Include private attributes starting with underscore include_properties: Include instance properties with assigned values

required
Output Formatting

include_class_name: Include class name in object representations sort_keys: Sort dictionary keys alphabetically sort_iterables: Sort items in sequences, sets

required
Advanced Controls

opts: DictifyOptions instance for advanced configuration. Individual parameters override corresponding options fields.

required

Returns:

Type Description
Any

Human-readable dictionary representation preserving built-in types and

Any

converting objects to dictionaries

Examples:

>>> # Basic object conversion
>>> class Person:
...     def __init__(self, name, age):
...         self.name = name
...         self.age = age
>>> obj = Person("Alice", 7)
>>> dictify(obj)
{'name': 'Alice', 'age': 7}
>>> # Include class information for debugging
>>> dictify(obj, include_class_name=True)
{'name': 'Alice', 'age': 7, '__class_name__': 'Person'}
>>> # Control recursion depth and size limits
>>> d = dictify(obj, max_depth=5, max_items=100)
>>> # Include everything for debugging
>>> d = dictify(obj, include_private=True, include_none=True,
...             include_properties=True, include_class_name=True)
>>> # Sorted output for consistent display
>>> d = dictify(obj, sort_keys=True, sort_iterables=True)
>>> # Size limits for large strings/bytes
>>> d = dictify(obj, max_str_len=200, max_bytes=512)
>>> # Override options configuration
>>> opts = DictifyOptions.debug()
>>> d = dictify(obj, max_depth=10, opts=opts)  # max_depth overrides opts
Note
  • Built-in types (int, str, list, dict, etc.) are preserved as-is
  • Custom objects are converted to dictionaries with their attributes
  • Collections are recursively processed up to max_depth levels
  • Properties that raise exceptions are automatically skipped
  • For specialized presets, use: DictifyOptions.debug(), .logging(), .serial()
  • For custom type handlers, use dictify_core() with add_type_handler()
  • For metadata injection and edge-case handlers, use dictify_core()
See Also

dictify_core: Core engine with full DictifyOptions configurability DictifyOptions: Configuration class with specialized preset factories

Source code in c108/dictify.py
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
def dictify(
    obj: Any,
    *,
    max_depth: int = 3,
    max_items: int | None = 100,
    max_str_len: int | None = 200,
    max_bytes: int | None = 512,
    include_none: bool = False,
    include_private: bool = False,
    include_properties: bool = False,
    include_class_name: bool = False,
    sort_keys: bool = False,
    sort_iterables: bool = False,
    opts: DictifyOptions | None = None,
) -> Any:
    """
    Convert Python objects to human-readable dictionaries with common customizations.

    Simplified wrapper around dictify_core() for everyday use cases: CLI printing,
    basic logging, and object inspection. Provides direct parameter access to the
    most common conversion options with sensible defaults (DictifyOptions()).

    For specialized presets (logging, debugging, serialization) or advanced features
    (custom type handlers, edge-case processing, metadata injection), use dictify_core()
    with DictifyOptions.

    Args:
        obj: Object to convert to dictionary representation

        Depth & Size Controls:
            max_depth: Maximum recursion depth for nested objects (default: 3)
            max_items: Maximum items in collections before trimming (default: 100).
                       None = no limit (process entire collection).
            max_str_len: String truncation limit (default: 200), None = no truncation
            max_bytes: Bytes object truncation limit (default: 512), None = no truncation

        Attribute Filtering:
            include_none: Include attributes and dictionary items with None values
            include_private: Include private attributes starting with underscore
            include_properties: Include instance properties with assigned values

        Output Formatting:
            include_class_name: Include class name in object representations
            sort_keys: Sort dictionary keys alphabetically
            sort_iterables: Sort items in sequences, sets

        Advanced Controls:
            opts: DictifyOptions instance for advanced configuration.
                     Individual parameters override corresponding options fields.

    Returns:
        Human-readable dictionary representation preserving built-in types and
        converting objects to dictionaries

    Examples:
        >>> # Basic object conversion
        >>> class Person:
        ...     def __init__(self, name, age):
        ...         self.name = name
        ...         self.age = age
        >>> obj = Person("Alice", 7)
        >>> dictify(obj)
        {'name': 'Alice', 'age': 7}

        >>> # Include class information for debugging
        >>> dictify(obj, include_class_name=True)
        {'name': 'Alice', 'age': 7, '__class_name__': 'Person'}

        >>> # Control recursion depth and size limits
        >>> d = dictify(obj, max_depth=5, max_items=100)

        >>> # Include everything for debugging
        >>> d = dictify(obj, include_private=True, include_none=True,
        ...             include_properties=True, include_class_name=True)

        >>> # Sorted output for consistent display
        >>> d = dictify(obj, sort_keys=True, sort_iterables=True)

        >>> # Size limits for large strings/bytes
        >>> d = dictify(obj, max_str_len=200, max_bytes=512)

        >>> # Override options configuration
        >>> opts = DictifyOptions.debug()
        >>> d = dictify(obj, max_depth=10, opts=opts)  # max_depth overrides opts


    Note:
        - Built-in types (int, str, list, dict, etc.) are preserved as-is
        - Custom objects are converted to dictionaries with their attributes
        - Collections are recursively processed up to max_depth levels
        - Properties that raise exceptions are automatically skipped
        - For specialized presets, use: DictifyOptions.debug(), .logging(), .serial()
        - For custom type handlers, use dictify_core() with add_type_handler()
        - For metadata injection and edge-case handlers, use dictify_core()

    See Also:
        dictify_core: Core engine with full DictifyOptions configurability
        DictifyOptions: Configuration class with specialized preset factories
    """

    # Build options from parameters
    opts = _dictify_opts(opts)

    include_class_name = bool(include_class_name)
    include_none = bool(include_none)

    # Apply parameter overrides
    opts = opts.merge(
        max_depth=max_depth,
        max_items=max_items,
        max_str_len=max_str_len,
        max_bytes=max_bytes,
        include_none_attrs=include_none,
        include_none_items=include_none,
        include_private=bool(include_private),
        include_properties=bool(include_properties),
        sort_keys=bool(sort_keys),
        sort_iterables=bool(sort_iterables),
        class_name=ClassNameOptions(in_expand=include_class_name, in_to_dict=include_class_name),
    )

    return dictify_core(obj, opts=opts)

dictify_core(obj, *, opts=None)

Convert any Python object to a human-readable dictionary representation with full control.

This is the main conversion engine of the dictify module, providing tunable object representation for CLI printing, logging, debugging, and serialization. Handles arbitrary Python objects by preserving primitives, intelligently processing collections, expanding object attributes, and offering extensive customization through DictifyOptions.

Use DictifyOptions() for basic conversion or its specialized preset factories for logging/debugging/serialization scenarios.

Processing Pipeline
  1. Skip Types: Primitives returned unchanged
  2. Edge Case Handling:
  3. max_depth < 0: Raw mode via raw() chain
  4. max_depth = 0: Terminal mode via terminal() chain
  5. Type Handler Resolution: Custom processors via inheritance hierarchy
  6. Object Hook Processing: to_dict() method calls based on hook_mode
  7. Collection Processing: Sequences, mappings, sets, views, other iterables
  8. Object Expansion: Attribute extraction with filtering rules

Parameters:

Name Type Description Default
obj Any

Any Python object to convert to dictionary representation

required
opts DictifyOptions | None

DictifyOptions instance controlling all conversion behaviors. Default DictifyOptions() used if None.

None
_seen

Internal parameter for cycle detection. Maps object IDs to sentinel values. DO NOT pass this parameter - it's managed automatically during recursion.

required

Returns:

Type Description
Any

Human-readable dictionary representation of the object, or processed result

Any

from custom handlers.

Handler Precedence (Normal Processing, max_depth > 0): 1. Skip types bypassed (default: int, float, bool, complex, None) 2. Type handlers (exact type or inheritance-based via MRO) 3. Object to_dict() method (controlled by hook_mode) 4. Collection/mapping/sequence recursive processing 5. Object attribute expansion with filtering

Reference Tracking and Cycle Detection
  • Enabled by default via opts.track_references=True
  • Prevents infinite recursion on circular references
  • Tracks object identity using id() to detect reused/shared objects
  • First occurrence: Object expanded normally with optional id in dictify metadata
  • Subsequent occurrences: Returns {"ref": object_id} reference dict
  • Only tracks objects that undergo expansion (not skip_types or type_handler primitives)
  • Works across all container types: dicts, lists, nested objects
  • Enable id metadata via opts.meta.include_ref_id=True for debugging
Edge Case Processing
  • Raw Mode (max_depth < 0): raw() → obj.to_dict() → obj identity
  • Terminal Mode (max_depth = 0): terminal() → type_handlers → obj.to_dict() → obj dentity
Collection Processing Features
  • Automatic size limiting with optional metadata injection
  • Comprehensive support for all Collection/MappingView types:
  • Sequences (list, tuple, str, bytes, etc.)
  • Mappings (dict, OrderedDict, etc.)
  • Sets (set, frozenset, etc.)
  • MappingViews (dict.keys(), dict.values(), dict.items())
  • Dict-like objects (custom classes with items() method)
  • Mapping keys skip recursive expansion
Object Expansion Rules
  • Private attributes included only if include_private=True
  • Properties included only if include_properties=True and accessible
  • None values filtered based on include_none_attrs setting
  • Class name injection controlled by class_name options
  • Meta injection controlled by options.meta flags
  • Attribute access exceptions automatically handled and skipped

Examples:

>>> # Basic conversion with defaults
>>> class Person:
...     def __init__(self, name, age):
...         self.name = name
...         self.age = age
>>> obj = Person("Alice", 7)
>>> result = dictify_core(obj)
>>> # Custom depth and terminal handling
>>> def terminal_handler(obj, opts):
...     return f"<{type(obj).__name__}:truncated>"
>>>
>>> opts = DictifyOptions(max_depth=5, handlers=Handlers(terminal=terminal_handler))
>>> result = dictify_core(obj, opts=opts)
>>> # Raw mode processing
>>> raw_opt = DictifyOptions(max_depth=-1)
>>> raw_result = dictify_core(obj, opts=raw_opt)  # Minimal processing
>>> # Collection size management
>>> size_opt = DictifyOptions(max_items=100, max_str_len=50)
>>> trimmed_result = dictify_core(obj, opts=size_opt)
>>> # Custom type handling with inheritance
>>> class DatabaseConnection: pass
>>> class PostgresConnection(DatabaseConnection): pass
>>>
>>> opts = (
...     DictifyOptions()
...     .add_type_handler(DatabaseConnection,
...                      lambda conn, opts: {"type": "db", "active": True})
... )
>>> # PostgresConnection inherits DatabaseConnection handler
>>> result = dictify_core(PostgresConnection(), opts=opts)

Strict object hook mode

>>> strict_opt = DictifyOptions(hook_mode="dict_strict")
>>> dictify_core(obj, opts=strict_opt)
Traceback (most recent call last):
...
TypeError: Class <Person> must implement to_dict() when hook_mode='HookMode.DICT_STRICT'
Special Behaviors
  • max_depth parameter controls recursion: N levels deep for collections, with object attributes processed at depth N-1
  • Skip types (int, float, bool, complex, None, range) bypass all processing
  • Default type handlers process str, bytes, bytearray, memoryview with size limits
  • Class name inclusion affects main processing with expand() and to_dict() only, not edge case handlers
  • Key sorting (if enabled) applies to main processing and to_dict() injection
  • Sets are converted to lists
  • Exception-raising properties automatically skipped during object expansion
  • MRO-based type handler resolution supports inheritance hierarchies

🚀 Performance Notes: - Collection trimming prevents memory issues with large datasets - Type handler caching optimizes repeated conversions - Shallow copying for depth management minimizes overhead - Early returns for skip types and edge cases improve efficiency

Source code in c108/dictify.py
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
def dictify_core(obj: Any, *, opts: DictifyOptions | None = None) -> Any:
    """
    Convert any Python object to a human-readable dictionary representation with full control.

    This is the main conversion engine of the dictify module, providing tunable object
    representation for CLI printing, logging, debugging, and serialization. Handles arbitrary
    Python objects by preserving primitives, intelligently processing collections, expanding
    object attributes, and offering extensive customization through DictifyOptions.

    Use DictifyOptions() for basic conversion or its specialized preset factories for
    logging/debugging/serialization scenarios.

    Processing Pipeline:
        1. Skip Types: Primitives returned unchanged
        2. Edge Case Handling:
           - max_depth < 0: Raw mode via raw() chain
           - max_depth = 0: Terminal mode via terminal() chain
        3. Type Handler Resolution: Custom processors via inheritance hierarchy
        4. Object Hook Processing: to_dict() method calls based on hook_mode
        5. Collection Processing: Sequences, mappings, sets, views, other iterables
        6. Object Expansion: Attribute extraction with filtering rules

    Args:
        obj: Any Python object to convert to dictionary representation
        opts: DictifyOptions instance controlling all conversion behaviors.
                Default DictifyOptions() used if None.
        _seen: Internal parameter for cycle detection. Maps object IDs to sentinel values.
               DO NOT pass this parameter - it's managed automatically during recursion.

    Returns:
        Human-readable dictionary representation of the object, or processed result
        from custom handlers.

    Handler Precedence (Normal Processing, max_depth > 0):
        1. Skip types bypassed (default: int, float, bool, complex, None)
        2. Type handlers (exact type or inheritance-based via MRO)
        3. Object to_dict() method (controlled by hook_mode)
        4. Collection/mapping/sequence recursive processing
        5. Object attribute expansion with filtering

    Reference Tracking and Cycle Detection:
        - Enabled by default via opts.track_references=True
        - Prevents infinite recursion on circular references
        - Tracks object identity using id() to detect reused/shared objects
        - First occurrence: Object expanded normally with optional __id__ in __dictify__ metadata
        - Subsequent occurrences: Returns {"__ref__": object_id} reference dict
        - Only tracks objects that undergo expansion (not skip_types or type_handler primitives)
        - Works across all container types: dicts, lists, nested objects
        - Enable __id__ metadata via opts.meta.include_ref_id=True for debugging

    Edge Case Processing:
        - Raw Mode (max_depth < 0): raw() → obj.to_dict() → obj identity
        - Terminal Mode (max_depth = 0): terminal() → type_handlers → obj.to_dict() → obj dentity

    Collection Processing Features:
        - Automatic size limiting with optional metadata injection
        - Comprehensive support for all Collection/MappingView types:
          * Sequences (list, tuple, str, bytes, etc.)
          * Mappings (dict, OrderedDict, etc.)
          * Sets (set, frozenset, etc.)
          * MappingViews (dict.keys(), dict.values(), dict.items())
          * Dict-like objects (custom classes with items() method)
        - Mapping keys skip recursive expansion

    Metadata Injection Features for recursive processing:
        - Injection based on detailed options.meta flags
        - Affects processing with expand() and to_dict()
        - Sequences/Sets/Iterables: Meta appended as the final element
        - Mappings, ItemViews, Mapping-like objects: Meta added under options.meta.key
        - Trimming meta for oversized collections (len > max_items) when options.meta.trim enabled
        - No Metadata injection in default raw(), terminal(), and type_handlers

    Object Expansion Rules:
        - Private attributes included only if include_private=True
        - Properties included only if include_properties=True and accessible
        - None values filtered based on include_none_attrs setting
        - Class name injection controlled by class_name options
        - Meta injection controlled by options.meta flags
        - Attribute access exceptions automatically handled and skipped

    Examples:
        >>> # Basic conversion with defaults
        >>> class Person:
        ...     def __init__(self, name, age):
        ...         self.name = name
        ...         self.age = age
        >>> obj = Person("Alice", 7)
        >>> result = dictify_core(obj)

        >>> # Custom depth and terminal handling
        >>> def terminal_handler(obj, opts):
        ...     return f"<{type(obj).__name__}:truncated>"
        >>>
        >>> opts = DictifyOptions(max_depth=5, handlers=Handlers(terminal=terminal_handler))
        >>> result = dictify_core(obj, opts=opts)

        >>> # Raw mode processing
        >>> raw_opt = DictifyOptions(max_depth=-1)
        >>> raw_result = dictify_core(obj, opts=raw_opt)  # Minimal processing

        >>> # Collection size management
        >>> size_opt = DictifyOptions(max_items=100, max_str_len=50)
        >>> trimmed_result = dictify_core(obj, opts=size_opt)

        >>> # Custom type handling with inheritance
        >>> class DatabaseConnection: pass
        >>> class PostgresConnection(DatabaseConnection): pass
        >>>
        >>> opts = (
        ...     DictifyOptions()
        ...     .add_type_handler(DatabaseConnection,
        ...                      lambda conn, opts: {"type": "db", "active": True})
        ... )
        >>> # PostgresConnection inherits DatabaseConnection handler
        >>> result = dictify_core(PostgresConnection(), opts=opts)

        # Strict object hook mode
        >>> strict_opt = DictifyOptions(hook_mode="dict_strict")
        >>> dictify_core(obj, opts=strict_opt)
        Traceback (most recent call last):
        ...
        TypeError: Class <Person> must implement to_dict() when hook_mode='HookMode.DICT_STRICT'

    Special Behaviors:
        - max_depth parameter controls recursion: N levels deep for collections,
          with object attributes processed at depth N-1
        - Skip types (int, float, bool, complex, None, range) bypass all processing
        - Default type handlers process str, bytes, bytearray, memoryview with size limits
        - Class name inclusion affects main processing with expand() and to_dict() only, not edge case handlers
        - Key sorting (if enabled) applies to main processing and to_dict() injection
        - Sets are converted to lists
        - Exception-raising properties automatically skipped during object expansion
        - MRO-based type handler resolution supports inheritance hierarchies

    🚀 Performance Notes:
        - Collection trimming prevents memory issues with large datasets
        - Type handler caching optimizes repeated conversions
        - Shallow copying for depth management minimizes overhead
        - Early returns for skip types and edge cases improve efficiency
    """
    return _dictify_core(obj, opts=opts, _seen=None)