How can I preserve 0 and False while replacing only None values in a Python dictionary?

I am building a dictionary from optional keyword arguments.

Currently I have code like this:

payload = {
    "premium": kwargs.get("premium") or "",
    "comments": kwargs.get("comments") or "",
    "overridden": kwargs.get("overridden", False),
}

The problem is that using or "" also replaces valid falsy values.

For example:

kwargs = {
    "premium": 0,
    "comments": None,
    "overridden": False,
}

I want the resulting dictionary to be:

{
    "premium": 0,
    "comments": "",
    "overridden": False,
}

But:

kwargs.get("premium") or ""

returns "" when premium is 0.

I only want to replace the value when it is actually None, while preserving valid values such as:

0
False
""

I could write this for every field:

premium = kwargs.get("premium")
premium = "" if premium is None else premium

but I have many fields in the payload.

Is there a clean Pythonic way to handle this without treating every falsy value as missing?

Asked AI also and did not help at all.

def preview_kwargs_from_payload(data: dict | None) -> dict[str, Any]:
    """Map a submit-preview / submit-enrichment request body onto builder kwargs."""
    data = data or {}
    return {
        "decision": data.get("decision"),
        "comments": data.get("comments") or "",
        "product_line": data.get("productLine") or data.get("product_line") or "",
        "premium": data.get("premium") or "",
        "effective_date": data.get("effectiveDate") or data.get("effective_date"),
        "expiry_date": data.get("expiryDate") or data.get("expiry_date"),
        "notes": data.get("notes") or "",
        "overridden": data.get("overridden", False),
        "reason_code": data.get("reasonCode") or data.get("reason_code") or "",
        "producer": data.get("producer"),
        "account": data.get("account") or {},
        "liability": data.get("liability") or {},
        "propertydata": (
            data.get("property")
            or data.get("property_data")
            or data.get("propertydata")
            or {}
        ),
        "locations": data.get("locations") or [],
        "documents": data.get("documents") or [],
    }


def build_guidewire_enrichment(data: dict | None = None, **kwargs: Any) -> dict:
    """Enrich the new submit payload's nested premises coverages with Kimi.

    Accepts the same body as submit-preview. ``subject_of_insurance``,
    ``amount``, ``ded``, and ``causes_of_losses`` are replaced on each
    ``property.premises.*.coverages[]`` row when Kimi evidences them. The
    rest of the payload is returned unchanged; preview/submit builders are
    not invoked.
    """
    payload = copy.deepcopy(data if data is not None else {})
    if kwargs and not payload:
        payload = {
            "decision": kwargs.get("decision"),
            "comments": kwargs.get("comments") or "",
            "productLine": kwargs.get("product_line") or "",
            "premium": kwargs.get("premium") or "",
            "effectiveDate": kwargs.get("effective_date"),
            "expiryDate": kwargs.get("expiry_date"),
            "notes": kwargs.get("notes") or "",
            "overridden": kwargs.get("overridden", False),
            "reasonCode": kwargs.get("reason_code") or "",
            "producer": kwargs.get("producer"),
            "account": copy.deepcopy(kwargs.get("account") or {}),
            "liability": copy.deepcopy(kwargs.get("liability") or {}),
            "property": copy.deepcopy(kwargs.get("propertydata") or {}),
            "locations": copy.deepcopy(kwargs.get("locations") or []),
            "documents": copy.deepcopy(kwargs.get("documents") or []),
        }
    propertydata = (
        payload.get("property")
        or payload.get("property_data")
        or payload.get("propertydata")
        or {}
    )
    if isinstance(propertydata, dict):
        enrich_property_coverages(propertydata)
        payload["property"] = propertydata
    return payload

If you omit the key from the dictionary instead of assigning to None, you can use the second argument of dict.get. So kwargs.get("premium", "") will return "" if "premium" not in kwargs, so the example of 0 would be fine. This would be my solution since what if None is a valid value?

If the key must always be present, I would personally create a helper function that does the None check and returns a default value when needed.

def foo(**kwargs):
    def kwarg_or_default(key, default = ""):
        return val if (val := kwargs.get(key)) is not None else default

    payload = {
        "premium": kwarg_or_default("premium"),
        "comments": kwarg_or_default("comments"),
        "overridden": kwarg_or_default("overridden"),
    }

A simple interior function may be sufficient for your needs here.

The condition val is not None gets evaluated first, but val has not been initialized yet.
I should be val if (val := kwargs.get('key')) is not None else default

Thanks for the correction. I wrote this on mobile as soon as I woke up haha

The or operator cares only whether something is truthy or falsey, and the get method returns the default you give it if the dict doesn’t have an entry for that key.

For any other behaviour, you’re going to have to write something yourself.

A helper function would be a good idea:

def get_non_none(dct, key, default):
    value = dct.get(key)
    return default if value is None else value

I usually just drop all items that are set to the sentinel (in your case None)

kwargs = {k: v for k, v in kwargs.items() if v is not None}

... # process normally with .get(k, default)

Yes,
Additionally, once you’ve dropped the undesired items, you can have a dictionary of default values aside and simply do :

desired_dict = default_dict | filtered_dict

Preserving falsy values was already the subject of the very discussed PEP505

You can also do that in the comprehension line:

kwargs = defaults | {k: v for k, v in kwargs.items() if v is not None)

You can also just make a function that does this with any Sentinel:

def drop(_dict: dict[str, Any], /, *sentinels: object) -> dict[str, Any]:
    return {k: v for k, v in _dict.items() if not any(v is s for s in sentinels)}


_Empty = object()


def func(**kwargs: Any):
    kwargs = defaults | drop(kwargs, None, _Empty)
    ... # handle fully populated defaults with additional kwargs

This is very useful if you’re moving around TypedDicts that have extra_items set to True or want to enforce a set of keys.

Doing this means you can safely access keys in the TypedDict at runtime even if something ends up missing them.

Here’s a more complete example with some casting and a TypedDict to define the keys:

from collections.abc import Mapping
from types import MappingProxyType
from typing import Any, Unpack, TypedDict, cast

class Options(TypedDict, total=False, extra_items=Any):
    a: int
    b: str

# frozendict can replace MappingProxyType 
# we just don't want to accidentally change this
DEFAULTS = cast(Options, MappingProxyType({
    'a': -1,
    'b': 'Hello'
}))


def drop[T: Mapping[Any, Any]](_opts: T, /, *sentinels: object) -> T:
    return cast(T, {
        k: v 
        for k, v in _opts.items() 
        if not any(v is s for s in sentinels)
    })


_Empty = object()


def parse(**kwargs: Unpack[Options]) -> Options:
    kwargs = DEFAULTS | drop(kwargs, None, _Empty)
    return kwargs
>>> parse(a=2, b='World', c=None, d=_Empty)
{'a': 2, 'b': 'World'}
>>> parse(c='Extra', d=None)
{'a': -1, 'b': 'Hello', 'c': 'Extra'}