Tiny conditional helpers for dict, list, and set — skip None, merge nested dicts, pick keys
Project description
iffy
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"] = xrepeated five times.conditional_setitem(d, **optional_fields)collapses it to one line. dict.updateclobbers nested values —{"a": {"x": 1}}.update({"a": {"y": 2}})gives{"a": {"y": 2}}, not{"a": {"x": 1, "y": 2}}.update_dict_recursivemerges 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_raisegives 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e4df03606abd1d7dbd472dd1bc0a560f43c0fa173badb28bf843a15a5e46cd27
|
|
| MD5 |
be8baac29875714da6b180aea4bb6eb1
|
|
| BLAKE2b-256 |
78c0e5f6ec51d84b1d9b0aad4cd06b0d2f30f064d9f4457ad4057dcb9a9a3f53
|
Provenance
The following attestation bundles were made for iffy-1.0.0.tar.gz:
Publisher:
publish-pypi.yml on miriada-io/iffy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
iffy-1.0.0.tar.gz -
Subject digest:
e4df03606abd1d7dbd472dd1bc0a560f43c0fa173badb28bf843a15a5e46cd27 - Sigstore transparency entry: 1358404982
- Sigstore integration time:
-
Permalink:
miriada-io/iffy@b58506d855068924d0c7197dbf9344ff4ad90ecf -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/miriada-io
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@b58506d855068924d0c7197dbf9344ff4ad90ecf -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7239cd54a4fd19f97d5e74b547ec04ecc2fcb70247345e657ef3e6a634c8e7d7
|
|
| MD5 |
3b08359b790c662fbf872543dbc952b0
|
|
| BLAKE2b-256 |
cbb28c861de7124725bfc4c00f8a6b75278f017425d09b2528913f0e55515d5b
|
Provenance
The following attestation bundles were made for iffy-1.0.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on miriada-io/iffy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
iffy-1.0.0-py3-none-any.whl -
Subject digest:
7239cd54a4fd19f97d5e74b547ec04ecc2fcb70247345e657ef3e6a634c8e7d7 - Sigstore transparency entry: 1358404989
- Sigstore integration time:
-
Permalink:
miriada-io/iffy@b58506d855068924d0c7197dbf9344ff4ad90ecf -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/miriada-io
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@b58506d855068924d0c7197dbf9344ff4ad90ecf -
Trigger Event:
release
-
Statement type: