128

I am using urllib.urlencode to build web POST parameters, however there are a few values I only want to be added if a value other than None exists for them.

apple = 'green'
orange = 'orange'
params = urllib.urlencode({
    'apple': apple,
    'orange': orange
})

That works fine, however if I make the orange variable optional, how can I prevent it from being added to the parameters? Something like this (pseudocode):

apple = 'green'
orange = None
params = urllib.urlencode({
    'apple': apple,
    if orange: 'orange': orange
})

I hope this was clear enough, does anyone know how to solve this?

user1814016
  • 2,273
  • 5
  • 25
  • 28
  • If there's an acceptable default value, you could use the `'orange': orange if orange else default` syntax. – jpm Jan 10 '13 at 17:38

14 Answers14

134

You'll have to add the key separately, after the creating the initial dict:

params = {'apple': apple}
if orange is not None:
    params['orange'] = orange
params = urllib.urlencode(params)

Python has no syntax to define a key as conditional; you could use a dict comprehension if you already had everything in a sequence:

params = urllib.urlencode({k: v for k, v in (('orange', orange), ('apple', apple)) if v is not None})

but that's not very readable.

If you are using Python 3.9 or newer, you could use the new dict merging operator support and a conditional expression:

params = urllib.urlencode(
    {'apple': apple} | 
    ({'orange': orange} if orange is not None else {})
)

but I find readability suffers, and so would probably still use a separate if expression:

params = {'apple': apple}
if orange is not None:
    params |= {'orange': orange}
params = urllib.urlencode(params)

Another option is to use dictionary unpacking, but for a single key that's not all that more readable:

params = urllib.urlencode({
    'apple': apple,
    **({'orange': orange} if orange is not None else {})
})

I personally would never use this, it's too hacky and is not nearly as explicit and clear as using a separate if statement. As the Zen of Python states: Readability counts.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 3
    For Python 3.5 and up: since [PEP-0448](https://www.python.org/dev/peps/pep-0448/) was implemented (proposed 29-Jun-2013), https://stackoverflow.com/a/55341342/563970 should be the answer – Bart Apr 10 '20 at 09:05
  • 1
    @Bart: that's very much a stylistic choice. For just one key, using `**({key: value} if test else {})` is really not more readable. – Martijn Pieters Apr 10 '20 at 11:22
  • 1
    Sure it is a stylistic choice, and for a single option it may be overkill. I've been using it to add `{key: value}` pairs to a nested dict where both key and value were derived (composed) from other keys and values. Doing this the `if something: ...` way would definitely _decrease_ readability in my case (due to the nesting which would then have to be applied twice or more). YMMV on a case-by-case basis here. – Bart Apr 20 '20 at 12:58
  • Quick illustration: In my case today, my conditional dict key is right in the middle of a big structure of nested dict and list literals (a mongoDB aggregation pipeline). It's REALLY helpful to have the conditional in the dict's place within the structure (although tomorrow I might decide that it looks too much like an injection vulnerability!). – Dave Gregory Jul 14 '20 at 18:52
40

To piggyback on sqreept's answer, here's a subclass of dict that behaves as desired:

class DictNoNone(dict):
    def __setitem__(self, key, value):
        if key in self or value is not None:
            dict.__setitem__(self, key, value)


d = DictNoNone()
d["foo"] = None
assert "foo" not in d

This will allow values of existing keys to be changed to None, but assigning None to a key that does not exist is a no-op. If you wanted setting an item to None to remove it from the dictionary if it already exists, you could do this:

def __setitem__(self, key, value):
    if value is None:
        if key in self:
            del self[key]
    else:
        dict.__setitem__(self, key, value)

Values of None can get in if you pass them in during construction. If you want to avoid that, add an __init__ method to filter them out:

def __init__(self, iterable=(), **kwargs):
    for k, v in iterable:
        if v is not None: self[k] = v
    for k, v in kwargs.iteritems():
        if v is not None: self[k] = v

You could also make it generic by writing it so you can pass in the desired condition when creating the dictionary:

class DictConditional(dict):
    def __init__(self, cond=lambda x: x is not None):
        self.cond = cond
    def __setitem__(self, key, value):
        if key in self or self.cond(value):
            dict.__setitem__(self, key, value)

d = DictConditional(lambda x: x != 0)
d["foo"] = 0   # should not create key
assert "foo" not in d
kindall
  • 178,883
  • 35
  • 278
  • 309
  • Thank you. I was able to figure it out using this in conjuntion with this answer http://stackoverflow.com/a/2588648/2860243 – Leonardo Ruiz Jul 18 '16 at 23:22
  • 1
    A new update method might not do it alone. CPython bypasses special methods when doing a dict-to-dict update (which it determines based on the memory structure of the object). You may need to avoid directly subclassing dict (you can set `__class__` to dict instead to pass isinstance checks). It's possible that this doesn't apply in this case (I was doing the inverse, transforming keys and values when extracted rather than when input), but I'm leaving this comment just in case it's helpful – DylanYoung Feb 28 '19 at 16:14
  • This works for adding new values. You need to override __init__ and process filter down kwargs values for None too if you want the constructor to work too. – Oli Apr 02 '19 at 09:47
  • Probably due to a different version (I'm using 3.10), but the third example didn't work. I've added a new answer with my example of how to filter any value that evaluates to `False` - https://stackoverflow.com/a/73692127/567059 – David Gard Sep 12 '22 at 16:05
19

Pretty old question but here is an alternative using the fact that updating a dict with an empty dict does nothing.

def urlencode_func(apple, orange=None):
    kwargs = locals().items()
    params = dict()
    for key, value in kwargs:
        params.update({} if value is None else {key: value})
    return urllib.urlencode(params)
lindsay.stevens.au
  • 519
  • 1
  • 4
  • 7
  • Oh, very neat. I like this answer the best! – RCross Aug 12 '16 at 11:00
  • Agreed, except for all that extra work you're doing by updating multiple times in a loop: get rid of the for loop and do this: `params.update({key: val for key, val in kwargs if val is not None})` – DylanYoung Feb 28 '19 at 16:17
19

One technique I suggest is using the dictionary unpacking operatior for this.

apple = 'green'
orange = None
params = urllib.urlencode({
    'apple': apple,
    **({ 'orange': orange } if orange else {})
})

Explanation

Basically, if orange is None, then the above dictionary simplifies to

{
    'apple': apple,
    **({})
}

# which results in just
{
    'apple': apple,
} 

Opposite goes with if orange is not None:

{
    'apple': apple,
    **({ "orange": orange })
}

# which results in just
{
    'apple': apple,
    'orange': orange
} 

Readablity is a downside for conditionally adding keys inline. It is possible to create a function that could help mediate the readability issue.

from typing import Callable

def cond_pairs(
        cond: bool, pairs: Callable[[], dict],
) -> dict:
    return pairs() if cond else {}

{
    'apple': apple,
    **cond_pairs(orange, lambda: { 'orange': orange })
}
kmui2
  • 2,227
  • 18
  • 19
5

I did this. Hope this help.

apple = 23
orange = 10
a = {
    'apple' : apple,
    'orange' if orange else None : orange
}

Expected output : {'orange': 10, 'apple': 23}

Although, if orange = None , then there will be a single entry for None:None. For example consider this :

apple = 23
orange = None
a = {
    'apple' : apple,
    'orange' if orange else None : orange
}

Expected Output : {None: None, 'apple': 23}

Nikhil Wagh
  • 1,376
  • 1
  • 24
  • 44
  • 1
    This is a neat trick. Then you only have one key to clear at the end: `None`. I'd suggest only doing the condition on the key (if you're worried about the value being there, just add `None: None` as the last line in the dict declaration), then afterwards do `del a[None]`. – DylanYoung Feb 28 '19 at 16:42
  • 1
    This is the best answer. Just add `a.pop(None)` and it`s perfect – raullalves Apr 23 '20 at 13:12
  • This is a bad practice. If the language doesn't support, better not to add extra operations to by pass this, (like a.pop, del a[None] and similars). – Fernando Martínez Jul 05 '20 at 10:15
  • You need to explicitly test for `is not None`, as stated in the question: *I only want to be added if a value other than `None`*. Try with `orange = ""` or `orange = 0`, which are values other than `None`. – Martijn Pieters Feb 15 '21 at 16:07
  • 1
    Other than that: **resist the urge to use tricks**. This code requires an additional statement (`a.pop(None)` or `if None in a: del a[None]`), and requires an explanation in a comment for future developers that have to maintain your code. – Martijn Pieters Feb 15 '21 at 16:08
3

You can clear None after the assignment:

apple = 'green'
orange = None
dictparams = {
    'apple': apple,
    'orange': orange
}
for k in dictparams.keys():
    if not dictparams[k]:
        del dictparams[k]
params = urllib.urlencode(dictparams)
sqreept
  • 5,236
  • 3
  • 21
  • 26
3

Another valid answer is that you can create you own dict-like container that doesn't store None values.

class MyDict:
    def __init__(self):
        self.container = {}
    def __getitem__(self, key):
        return self.container[key]
    def __setitem__(self, key, value):
        if value != None:
            self.container[key] = value
    def __repr__(self):
        return self.container.__repr__()

a = MyDict()
a['orange'] = 'orange';
a['lemon'] = None

print a

yields:

{'orange': 'orange'}
sqreept
  • 5,236
  • 3
  • 21
  • 26
3

I really like the neat trick in the answer here: https://stackoverflow.com/a/50311983/3124256

But, it has some pitfalls:

  1. Duplicate if tests (repeated for key and value)
  2. Pesky None: None entry in the resulting dict

To avoid this, you can do the following:

apple = 23
orange = None
banana = None
a = {
    'apple' if apple else None: apple,
    'orange' if orange else None : orange,
    'banana' if banana else None: banana,
    None: None,
}
del a[None]

Expected Output : {'apple': 23}

Note: the None: None entry ensures two things:

  1. The None key will always be present (del won't throw an error)
  2. The contents of 'None values' will never exist in the dict (in case you forget to del afterwards)

If you aren't worried about these things, you can leave it out and wrap the del in a try...except (or check if the None key is present before deling). To address number 2 alternatively, you could also put the conditional check on the value (in addition to the key).

DylanYoung
  • 2,423
  • 27
  • 30
  • 1
    Interesting, any reason for not doing this? (I'm asking because the answer is barely upvoted). I think it's not the easiest to read but it doesn't sound too crazy to me. – cglacet Aug 11 '22 at 14:08
  • It's a bit hard to read / understand what the intent is I would say is the main reason you may not want to use it. – DylanYoung Sep 26 '22 at 01:41
  • It also requires repeating the `if` test, in contrast to this answer https://stackoverflow.com/a/64988520/3124256 – DylanYoung Sep 26 '22 at 01:43
3

I find using a generator function to be easier to understand, and flexible enough. It also works with both Python 2 and 3.

def generate_request_items(apple, orange):
    yield "apple", apple
    if orange:
        yield "orange", orange
    # Add additional conditionals and yield statements here


apple = 'green'
orange = None
params = urllib.urlencode(dict(generate_request_items(apple, orange)))
Attila Viniczai
  • 644
  • 2
  • 8
2

You can deal with all optional items using a single condition by using a dictionary comprehension:

apple = "red"
orange = None
dictparams = {
    key: value for key, value in
    {
        "apple": apple,
        "orange": orange
    }.items()
    if value is not None
}

The dictparams result will not contain "orange" in this case, because orange is None:

{'apple': 'red'}
cglacet
  • 8,873
  • 4
  • 45
  • 60
Rob Smallshire
  • 1,450
  • 1
  • 15
  • 22
1

You can add the data and then filter it via a condition afterwards:

data = {
    "foo": None,
    "bar": "1234",
    "baz": None,
}
data = {k: v for k, v in data.items() if v is not None}

would result in:

data = {
    "bar": "1234",
}

See also this question/answers for filter/lambda functions on a dictionary:

How to filter a dictionary according to an arbitrary condition function?

ajrlewis
  • 2,968
  • 3
  • 33
  • 67
0
fruits = [("apple", get_apple()), ("orange", get_orange()), ...]

params = urllib.urlencode({ fruit: val for fruit, val in fruits if val is not None })
XORcist
  • 4,288
  • 24
  • 32
  • So we need a `getter` for each variable. Why not just do: `fruits={"apple", "orange"}; d=vars(); params = urllib.urlencode({ fruit: val for fruit, val in d.items() if fruit in fruits and val is not None })` – DylanYoung Oct 25 '19 at 14:08
0

There is a counter-intuitive but reliable hack, to reuse the other prop name you want to exclude it.

{
    'orange' if orange else 'apple': orange,
    'apple': apple,
}

In this case, the latter 'apple' will override the previous 'apple' effectively removing it. Note that the conditional expressions should go above the real ones.

Ilya Kharlamov
  • 3,698
  • 1
  • 31
  • 33
  • 1
    I won't suggest this, because it depends on the ordering in which you write the keys. It is prone to bugs. – Nikhil Wagh Apr 13 '20 at 09:09
  • Resist the urge to use 'clever tricks'. You'll not thank yourself later when you rename the `'apple'` key to `'pear'` and miss the first line, and so introduced a weird bug. Readability **counts**! – Martijn Pieters Feb 15 '21 at 16:10
  • @MartijnPieters Did I mention that it's a hack? It should be treated as a hack. – Ilya Kharlamov Feb 16 '21 at 18:39
0

Building of the answer from kindall, I'm using this to filter out not just None values, but anything that evaluates to False. This suits my scenario as I don't want anything that is empty, False or None.

class DictNoNone(dict):

    def __init__(self, iterable=(), **kwargs):
        for k, v in iterable:
            if v: self[k] = v
        for k, v in kwargs.items():
            if v: self[k] = v

    def __setitem__(self, key, value):
        if not value:
            if key in self:
                del self[key]
        else:
            dict.__setitem__(self, key, value)
David Gard
  • 11,225
  • 36
  • 115
  • 227