@@ -253,12 +253,16 @@ whose constructors only allow keyword arguments. For example:
253
253
254
254
``` python
255
255
class ExampleStructure :
256
+ required_param: str
257
+ struct_param: OtherStructure
258
+ optional_param: str | None
259
+
256
260
def __init__ (
257
261
self ,
258
262
* , # This prevents positional arguments
259
263
required_param : str ,
260
264
struct_param : OtherStructure,
261
- optional_param : Optional[ str ] = None ,
265
+ optional_param : str | None = None
262
266
):
263
267
self .required_param = required_param
264
268
self .struct_param = struct_param
@@ -270,14 +274,14 @@ class ExampleStructure:
270
274
" StructParam" : self .struct_param.as_dict(),
271
275
}
272
276
273
- if self .optional_param:
277
+ if self .optional_param is not None :
274
278
d[" OptionalParam" ] = self .optional_param
275
279
276
280
@ staticmethod
277
281
def from_dict (d : Dict) -> ExampleStructure:
278
282
return ExampleStructure(
279
283
required_param = d[" RequiredParam" ],
280
- struct_param = OtherStructure.from_dict(d[" StructParam" ])
284
+ struct_param = OtherStructure.from_dict(d[" StructParam" ]),
281
285
optional_param = d.get(" OptionalParam" ),
282
286
)
283
287
```
@@ -369,6 +373,204 @@ print(example_dict.get("OptionalParam")) # None
369
373
This is a small example of a minor annoyance, but one that you must always be
370
374
aware of when using dicts.
371
375
376
+ ### Default Values
377
+
378
+ Default values on structures are indicated by wrapping them in a simple class.
379
+
380
+ ``` python
381
+ class _DEFAULT :
382
+ def __init__ (self , wrapped : Any):
383
+ """ Wraps a value to signal it was provided by default.
384
+
385
+ These values will be immediately unwrapped in the associated
386
+ initializers so the values can be used as normal, the defaultedness
387
+ will then be tracked separately.
388
+ """
389
+ self ._wrapped = wrapped
390
+
391
+ @ property
392
+ def value (self ) -> Any:
393
+ # Prevent mutations from leaking by simply returning copies of mutable
394
+ # defaults. We could also just make immutable subclasses.
395
+ if isinstance (self ._wrapped, list ):
396
+ return list (self ._wrapped)
397
+ if isinstance (self ._wrapped, dict ):
398
+ return dict (self ._wrapped)
399
+ return self ._wrapped
400
+
401
+ def __repr__ (self ) -> str :
402
+ return f " _DEFAULT( { repr (self ._wrapped)} ) "
403
+
404
+ def __str__ (self ) -> str :
405
+ return str (self ._wrapped)
406
+
407
+
408
+ D = TypeVar(" D" )
409
+
410
+
411
+ def _default (value : D) -> D:
412
+ """ Wraps a value to signal it was provided by default.
413
+
414
+ These values will be immediately unwrapped in the associated
415
+ initializers so the values can be used as normal, the defaultedness
416
+ will then be tracked separately.
417
+
418
+ We use this wrapper function for brevity, but also because many completion
419
+ tools will show the code of the default rather than the result, and
420
+ `_default(7)` is a bit more clear than `cast(int, _DEFAULT(7))`.
421
+ """
422
+ return cast(D, _DEFAULT(value))
423
+
424
+
425
+ class StructWithDefaults :
426
+ default_int: int
427
+ default_list: list
428
+
429
+ def __init__ (
430
+ self ,
431
+ * ,
432
+ default_int : int = _default(7 ),
433
+ default_list : list = _default([]),
434
+ ):
435
+ self ._has: dict[str , bool ] = {}
436
+ self ._set_default_attr(" default_int" , default_int)
437
+ self ._set_default_attr(" default_list" , default_list)
438
+
439
+ def _set_default_attr (self , name : str , value : Any) -> None :
440
+ # Setting the attributes this way saves a ton of lines of repeated
441
+ # code.
442
+ if isinstance (value, _DEFAULT ):
443
+ object .__setattr__ (self , name, value.value())
444
+ self ._has[name] = False
445
+ else :
446
+ setattr (self , name, value)
447
+
448
+ def __setattr__ (self , name : str , value : Any) -> None :
449
+ object .__setattr__ (self , name, value)
450
+ self ._has[name] = True
451
+
452
+ def _hasattr (self , name : str ) -> bool :
453
+ if self ._has[name]:
454
+ return True
455
+ # Lists and dicts are mutable. We could make immutable variants, but
456
+ # that's kind of a bad experience. Instead we can just check to see if
457
+ # the value is empty.
458
+ if isinstance ((v := getattr (self , name)), (dict , list )) and len (v) == 0 :
459
+ self ._has[name] = True
460
+ return True
461
+ return False
462
+ ```
463
+
464
+ One of the goals of customizable default values is to reduce the amount of
465
+ nullable members that are exposed. With that in mind, the typical strategy of
466
+ assigning the default value to ` None ` can't be used since that implicitly adds
467
+ ` None ` to the type signature. That would also make IntelliSense marginally
468
+ worse since you can't easily see the actual default value.
469
+
470
+ Instead, a default wrapper is used. The presence of the wrapper signifies to
471
+ the initializer function that a default was used. The value is then immediately
472
+ unwrapped so it can be used where needed. The defaultedness is stored in an
473
+ internal dict that is updated whenever the property is re-assigned. A private
474
+ function exists to give the serializer this information.
475
+
476
+ To make this wrapper class pass the type checker, it is simply "cast" to the
477
+ needed type. This isn't a problem since the true value is immediately unwrapped
478
+ in the initializer. A wrapper function performs the actual wrapping. This has
479
+ the advantage of not requiring the type signature to be repeated since it can
480
+ be inferred from the type of the function's input. It also has the advantage of
481
+ looking a bit nicer in many IntelliSense tools, who show the code assigned to
482
+ as the default value rather than the resolved value.
483
+
484
+ #### Alternative: Subclassing
485
+
486
+ One potential alternative is to create "default" subclasses of the various
487
+ defaultable types.
488
+
489
+ ``` python
490
+ class _DEFAULT_INT (int ):
491
+ pass
492
+
493
+
494
+ class _DEFAULT_STR (str ):
495
+ pass
496
+
497
+
498
+ class WithWrappers :
499
+ def __init__ (
500
+ self ,
501
+ * ,
502
+ default_int : int = _DEFAULT_INT(7 ),
503
+ default_str : str = _DEFAULT_STR(" foo" ),
504
+ ):
505
+ self .default_int = default_int
506
+ self .default_str = default_str
507
+ ```
508
+
509
+ The advantage of this is that it requires no upkeep and no lying to the type
510
+ system. These values are real, normal value that can be used everywhere their
511
+ base classes can. During serialization we can check if it's the default
512
+ type.
513
+
514
+ Unfortunately, this isn't wholly possible because not all of the defaultable
515
+ values can be subclassed. Neither ` bool ` nor ` NoneType ` can have subclasses,
516
+ so we'd need to create our own sentinel values. This risks unexpected behavior
517
+ if a customer were to use an ` is ` check.
518
+
519
+ #### Alternative: kwargs
520
+
521
+ Another possible alternative is to use the keyword arguments dictionary
522
+ feature.
523
+
524
+ ``` python
525
+ class _WithKwargsType (TypedDict ):
526
+ default_int: NotRequired[int ]
527
+ default_str: NotRequired[str ]
528
+ default_list: NotRequired[list[str ]]
529
+
530
+
531
+ class WithKwargs :
532
+ default_int: int
533
+ default_str: str
534
+ default_list: list[str ]
535
+
536
+ # This syntax for typing kwargs requires PEP 692
537
+ def __init__ (self , ** kwargs : ** _WithKwargsType):
538
+ self ._has = {}
539
+ self .default_int = kwargs.get(" default_int" , 7 )
540
+ self ._has[" default_int" ] = " default_int" in kwargs
541
+ self .default_str = kwargs.get(" default_str" , " foo" )
542
+ self ._has[" default_str" ] = " default_str" in kwargs
543
+ self .default_list = kwargs.get(" default_list" , [])
544
+ self ._has[" default_list" ] = " default_list" in kwargs
545
+
546
+ def __setattr__ (self , name : str , value : Any) -> None :
547
+ object .__setattr__ (self , name, value)
548
+ self ._has[name] = True
549
+
550
+ def _hasattr (self , name : str ) -> bool :
551
+ if self ._has[name]:
552
+ return True
553
+ if isinstance ((v := getattr (self , name)), (dict , list )) and len (v) == 0 :
554
+ self ._has[name] = True
555
+ return True
556
+ return False
557
+ ```
558
+
559
+ This leverages another feature of python that natively allows for presence
560
+ checks. The kwargs dictionary implicitly contains that metadata because keys
561
+ not set simply aren't present. This otherwise uses the same internal dict
562
+ mechanism to continue to keep track of defaultedness.
563
+
564
+ The major disadvantage to this is that it essentially forgoes IntelliSense
565
+ and type checking until [ PEP 692] ( https://peps.python.org/pep-0692/ ) lands.
566
+ This isn't expected to happen until 3.12 at the earliest, which is expected
567
+ in late 2023 / early 2024. Then the tools need to be updated for support,
568
+ which isn't straight-forward.
569
+
570
+ Another disadvantage is that it excludes the ability to include the default
571
+ value in the IntelliSense since the typing of kwargs relies on TypedDicts
572
+ which don't support default values.
573
+
372
574
## Errors
373
575
374
576
Modeled errors are specialized structures that have a ` code ` and ` message ` .
0 commit comments