Repository navigation
Closed
Description
Description
In my project, I have structs that contains list fields. I would like to omit these fields when the list is empty to reduce the size of the saved files.
When using the list constructor as the default factory or a default empty list, it works fine. However, the former conflicts with the type linter, and I want to avoid the latter (shared across all instances).
When I use a custom factory function, the field always appear in the output.
It may be the correct behavior, but I did not find any mention of this in the docs.
Reproduction
I ran this using Python 3.14 and msgspec 0.21.1.
from __future__ import annotations
import msgspec
class Fruit(msgspec.Struct):
name: str = msgspec.field(name="name")
def fruits_factory() -> list[Fruit]:
return []
class Basket(msgspec.Struct, omit_defaults=True):
size: int = msgspec.field(name="size")
material: str = msgspec.field(name="material", default="wicker")
fruits: list[Fruit] = msgspec.field(name="fruits", default_factory=fruits_factory)
class BasketWithListConstructor(msgspec.Struct, omit_defaults=True):
size: int = msgspec.field(name="size")
material: str = msgspec.field(name="material", default="wicker")
fruits: list[Fruit] = msgspec.field(name="fruits", default_factory=list) # Type error because list is untyped
class BasketWithDefaultValue(msgspec.Struct, omit_defaults=True):
size: int = msgspec.field(name="size")
material: str = msgspec.field(name="material", default="wicker")
fruits: list[Fruit] = msgspec.field(name="fruits", default=[]) # Shared mutable default value, should be avoided
if __name__ == "__main__":
basket = Basket(size=5)
print(msgspec.json.encode(basket))
# Expected output: b'{"size":5}'
# Actual output: b'{"size":5,"fruits":[]}'
basket_with_list_constructor = BasketWithListConstructor(size=5)
print(msgspec.json.encode(basket_with_list_constructor))
# Output: b'{"size":5}'
basket_with_default_value = BasketWithDefaultValue(size=5)
print(msgspec.json.encode(basket_with_default_value))
# Output: b'{"size":5}'
print(f"Using msgspec {msgspec.__version__}")Metadata
Metadata
Assignees
Labels
No labels
Activity
Siyet commentedon Jun 16, 2026
This is intended behavior, though it wasn't documented, which I'll fix.
omit_defaultsis optimized for performance: the "value equals default" check is O(1) by type and length and never calls user code. So fordefault_factory, omitting an empty collection only works when the factory is a builtin constructor (list/dict/set, plustuple/frozenset). A custom factory is opaque to msgspec: knowing what it returns would mean calling it on every encode (too expensive) or once at class-creation time (side effects at import, and unsound for a dynamic factory). The same performance constraint is why #645 is unaddressed.On your two reasons for preferring a custom factory:
default=[]is safe in msgspec. Unlikedataclasses, msgspec does not share the mutable default between instances; each instance gets a fresh list. So there's no shared-mutable footgun here.The type-checker error on
default_factory=listis resolved by the field annotation. The element type comes from the annotation, not the factory:This both omits when empty and type checks.
I'd recommend that spelling. Documenting the
default_factorylimitation in #1076.Leo-Marbehan commentedon Jun 16, 2026
Thank you!