Skip to main content

Tiny conditional helpers for dict, list, and set — skip None, merge nested dicts, pick keys

Project description

iffy

PyPI Python Tests License

Small utilities for conditional dict, list, and set operations — skip None, merge nested dicts, pick keys, collect or raise exceptions.

Installation

pip install iffy

Requires Python 3.11+.

Why iffy?

Assembling a dict or list from optional values is a surprisingly verbose corner of Python. The stdlib makes you choose between two bad options: a noisy chain of if-statements, or a comprehension that inlines the predicate into every call site.

  • Building a request payload from optional form fields typically looks like if x is not None: d["x"] = x repeated five times. conditional_setitem(d, **optional_fields) collapses it to one line.
  • dict.update clobbers nested values — {"a": {"x": 1}}.update({"a": {"y": 2}}) gives {"a": {"y": 2}}, not {"a": {"x": 1, "y": 2}}. update_dict_recursive merges deeply instead.
  • Extracting a subset of keys from a dict is usually a comprehension or operator.itemgetter. PickableDict(...).pick([...]) reads as plain English.
  • In validation or batch-processing code you often want to either raise on the first error or collect errors per-item, depending on context. write_to_dict_or_raise gives you both modes behind a single call.

Plain functions on plain dict, list, and set. Each helper does one thing, so there is nothing to learn beyond its signature.

Quick Start

from iffy import PickableDict, conditional_setitem, update_dict_recursive

# Build a PATCH payload — only send fields that actually changed.
patch: dict = {}
conditional_setitem(patch, name="alice", email=None, age=30, bio=None)
assert patch == {"name": "alice", "age": 30}  # None fields are dropped, not sent as null

# Merge user overrides on top of defaults, preserving nested keys.
config = {"timeouts": {"connect": 5, "read": 30}, "retries": 3}
overrides = {"timeouts": {"read": 60}}
update_dict_recursive(config, overrides)
assert config == {"timeouts": {"connect": 5, "read": 60}, "retries": 3}

# Expose only a whitelisted subset of a record.
user = PickableDict({"id": 1, "name": "alice", "password_hash": "...", "internal_id": "xyz"})
assert user.pick(["id", "name"]) == {"id": 1, "name": "alice"}

Overview

Conditional operations: add_if | append_if | setitem_if | conditional_add | conditional_append | conditional_setitem

Dict utilities: PickableDict | update_dict_recursive

Error handling: write_to_dict_or_raise

Predicates: is_none | is_not_none


Conditional Operations

Two families of helpers for writing into a container when a condition holds.

The *_if functions take a single boolean and apply it to all values — "if this flag is set, write everything". The conditional_* functions take a per-value predicate (default: is_not_none) and apply it to each value individually — "write each value that passes the check". Pick whichever matches your branching style.

add_if

Add all given values to a set when condition is truthy.

from iffy import add_if

tags = {"read"}
add_if(user.is_admin, tags, "write", "delete")
# if user.is_admin: tags == {"read", "write", "delete"}
# else:             tags == {"read"}

Equivalent to if condition: set_.update(args), but reads more naturally inline.

append_if

Append all given values to a list when condition is truthy.

from iffy import append_if

args = ["--verbose"]
append_if(debug_mode, args, "--trace", "--dump-on-error")
# if debug_mode: args == ["--verbose", "--trace", "--dump-on-error"]
# else:          args == ["--verbose"]

setitem_if

Set all given items in a MutableMapping when condition is truthy.

from iffy import setitem_if

request_body = {"limit": 10}
setitem_if(is_auth_required, request_body, user="guest", password="secret")
# if is_auth_required: request_body == {"limit": 10, "user": "guest", "password": "secret"}
# else:                request_body == {"limit": 10}

Use when: a whole block of fields shares a single gate — a feature flag, a role check, a privacy toggle, a dev-vs-prod switch.

conditional_add

Add each value to a set that passes a per-value predicate. Default predicate is is_not_none.

from iffy import conditional_add

# Default predicate drops None:
roles = {"user"}
conditional_add(roles, "admin", None, "editor", None)
# roles == {"user", "admin", "editor"}

# Custom predicate — e.g. skip empty strings with the builtin `bool`:
tags: set[str] = set()
conditional_add(tags, "", "python", "", "typing", _condition=bool)
# tags == {"python", "typing"}

conditional_append

Append each value to a list that passes a per-value predicate. Default predicate is is_not_none.

from iffy import conditional_append

def parse_int(s: str) -> int | None:
    try:
        return int(s)
    except ValueError:
        return None

numbers: list[int] = []
conditional_append(numbers, parse_int("42"), parse_int("abc"), parse_int("7"))
# numbers == [42, 7]  — "abc" failed to parse and was skipped

Use when: you are collecting results from lookups or parsers that may return None, and you want the successes in order without a separate if ... is not None per call.

conditional_setitem

Set each keyword argument in a MutableMapping whose value passes a per-value predicate. Default predicate is is_not_none.

from iffy import conditional_setitem

def build_query(name=None, status=None, created_after=None):
    q = {}
    conditional_setitem(q, name=name, status=status, created_after=created_after)
    return q

build_query(name="alice")
# {"name": "alice"} — status and created_after dropped because they're None

Use when: building API request bodies, query parameters, or partial update payloads where absent fields must not appear as null.

All three conditional_* functions accept a _condition keyword to override the default is_not_none check:

def is_positive(x) -> bool:
    return isinstance(x, (int, float)) and x > 0

metrics: dict[str, float] = {}
conditional_setitem(metrics, requests=0, latency_ms=42, errors=-1, _condition=is_positive)
# metrics == {"latency_ms": 42}  — zero and negative values filtered out

Dict Utilities

PickableDict

A dict subclass with a single extra method: .pick(keys). Returns a plain dict containing only the listed keys that exist in the source.

from iffy import PickableDict

user = PickableDict({"id": 1, "name": "alice", "password_hash": "...", "email": "a@b.c"})

user.pick(["id", "name"])
# {"id": 1, "name": "alice"}

# Missing keys are silently skipped — no KeyError:
user.pick(["id", "does_not_exist"])
# {"id": 1}

Use when: you have a bag-of-fields dict (ORM row, form payload, config) and you want to emit a whitelisted projection of it. The alternatives ({k: d[k] for k in keys if k in d}, operator.itemgetter with try/except) are either noisy or brittle.

update_dict_recursive

Like dict.update, but merges nested dicts instead of replacing them. Modifies target_dict in place.

from iffy import update_dict_recursive

defaults = {
    "db": {"host": "localhost", "port": 5432, "pool": {"min": 1, "max": 10}},
    "debug": False,
}
overrides = {
    "db": {"host": "prod.db", "pool": {"max": 50}},
    "debug": True,
}

update_dict_recursive(defaults, overrides)
# defaults == {
#     "db": {"host": "prod.db", "port": 5432, "pool": {"min": 1, "max": 50}},
#     "debug": True,
# }

Non-dict values replace in full. A dict replacing a non-dict (or vice versa) also replaces — the merge only recurses when both sides are dicts.

update_dict_recursive({"x": 1}, {"x": {"nested": True}})
# {"x": {"nested": True}}  — plain value replaced by a dict

Use when: layering configuration sources (defaults → file → env → CLI), merging partial patches into a document, or any time dict.update's shallow behavior silently drops fields.


Error Handling

write_to_dict_or_raise

Either records an exception in a dict under the given key, or re-raises it — depending on whether the dict is None.

from iffy import write_to_dict_or_raise

def validate_all(items, *, collect=True):
    errors: dict[str, Exception] | None = {} if collect else None
    for name, item in items.items():
        try:
            validate(item)
        except ValueError as e:
            write_to_dict_or_raise(errors, name, e)
    return errors

# Collect errors from every item, return them together:
validate_all(items, collect=True)
# {"item_3": ValueError(...), "item_7": ValueError(...)}

# Fail fast on the first error:
validate_all(items, collect=False)
# raises ValueError on the first bad item

Use when: the same validation or processing loop needs to run in two modes — aggregate-then-report in batch jobs, fail-fast in interactive use — without duplicating the loop.


Predicates

is_none / is_not_none

Thin wrappers around x is None / x is not None, useful as first-class callables.

from iffy import is_none, is_not_none

is_none(None)     # True
is_none(0)        # False
is_none("")       # False

is_not_none(None) # False
is_not_none(0)    # True

They exist because is is a keyword, not a function — so you can't pass it to filter, map, or use it as a default argument. is_not_none is the default _condition for all three conditional_* helpers.

list(filter(is_not_none, [1, None, 2, None, 3]))
# [1, 2, 3]

Deprecated Aliases

The following names are kept for backward compatibility. They emit a DeprecationWarning and simply forward their arguments — prefer the canonical names in new code. They will be removed in a future major release.

Deprecated Use instead
set_if(condition, mapping, **kwargs) setitem_if(condition, mapping, **kwargs)
conditional_set(mapping, _condition=..., **kwargs) conditional_setitem(mapping, _condition=..., **kwargs)

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

iffy-1.0.0.tar.gz (10.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

iffy-1.0.0-py3-none-any.whl (10.3 kB view details)

Uploaded Python 3

File details

Details for the file iffy-1.0.0.tar.gz.

File metadata

  • Download URL: iffy-1.0.0.tar.gz
  • Upload date:
  • Size: 10.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for iffy-1.0.0.tar.gz
Algorithm Hash digest
SHA256 e4df03606abd1d7dbd472dd1bc0a560f43c0fa173badb28bf843a15a5e46cd27
MD5 be8baac29875714da6b180aea4bb6eb1
BLAKE2b-256 78c0e5f6ec51d84b1d9b0aad4cd06b0d2f30f064d9f4457ad4057dcb9a9a3f53

See more details on using hashes here.

Provenance

The following attestation bundles were made for iffy-1.0.0.tar.gz:

Publisher: publish-pypi.yml on miriada-io/iffy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file iffy-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: iffy-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 10.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for iffy-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7239cd54a4fd19f97d5e74b547ec04ecc2fcb70247345e657ef3e6a634c8e7d7
MD5 3b08359b790c662fbf872543dbc952b0
BLAKE2b-256 cbb28c861de7124725bfc4c00f8a6b75278f017425d09b2528913f0e55515d5b

See more details on using hashes here.

Provenance

The following attestation bundles were made for iffy-1.0.0-py3-none-any.whl:

Publisher: publish-pypi.yml on miriada-io/iffy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page