Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dictionary with some mandatory keys as function input

I have a function that has a dictionary as an argument. I will pass various dictionaries to it that have more entries than the few used inside the function. Additionally, I would like to see in the function definition what keys are required. So I write

def fun(indict=dict(apple=None, pear=None)):

However, the function now accepts any input as indict. Is there a smart way for writing

any dictionary that has at least the keys 'apple' and 'pear' is accepted.

Something like

def fun(indict=dict(apple=NeedsToBeSpecified, pear=NeedsToBeSpecified)):
like image 403
Jan Avatar asked Jan 09 '14 08:01

Jan


3 Answers

In python3.x, you can use function annotations:

>>> def foo(indict: dict(apple=None, pear=None)):
...     print(indict)
... 
>>> foo(dict())
{}

You can even go crazy with the now more widely accepted (by the interpreter) Ellipsis literal

>>> def foo(indict: dict(apple=None, pear=None, extra_items=...)) -> int:
...     if any(x not in indict for x in ('apple', 'pear')):
...         raise ValueError('message here...')
...     print(indict)
...     return 3
... 
>>> foo({})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in foo
ValueError: message here...
>>> foo({'apple':6, 'pear':4})
{'pear': 4, 'apple': 6}
3
>>> foo({'apple':6, 'pear':4, 'carrot':30000})
{'carrot': 30000, 'pear': 4, 'apple': 6}
3

As you can see from my first example, the annotation it doesn't enforce anything. You'd have to perform the validation in the function itself although I suppose you could introspect the required keys from the annotations1 if you wanted to keep it DRY, but it's probably not worth the effort for just 2 keys...

In python2.x (and more traditionally), perhaps you'd just want to put the information in the docstring ;-) -- And I'd recommend you do that for python3.x as well since that's the traditional place to go looking for documentation ...

1keys = foo.__annotations__['indict'].keys() - {'extra_items'}

UPDATE Note that now with fancy things like mypy sitting around, this answer is maybe a little outdated. You might consider annotating with a TypedDict from mypy_extensions. That should set expectations for your users and maybe even help catch some bugs if you use a type-checker like mypy.

from mypy_extensions import TypedDict

class Apple:
    """Represent an Apple."""

class Pear:
    """Represent a Pear."""

# "annotation-type" for a dictionary that has an apple and pear key whose values are Apple and Pear instances.
FruitBowl = TypedDict("FruitBowl": {"apple": Apple, "Pear": Pear})

def foo(indict: FruitBowl) -> int:
    ...
like image 99
mgilson Avatar answered Oct 22 '22 05:10

mgilson


You could just check:

def fun(indict=dict(apple=None, pear=None)):
    if "apple" not in indict and "pear" not in indict:
        raise ValueError("'indict' must contain...")

However, you shouldn't really use a dictionary (or other mutable) default argument in Python; instead, prefer:

def fun(indict=None):
    if indict is None:
        indict = {"apple": None, "pear": None}
    elif "apple" not in indict...

Or you could use update to ensure both keys are always present, rather than forcing the caller to provide them:

def fun(indict=None):
    defdict = {"apple": None, "pear": None}
    if indict is  not None:
        defdict.update(indict)
    indict = defdict
like image 38
jonrsharpe Avatar answered Oct 22 '22 05:10

jonrsharpe


I see quite a few complicated answers for something that is really trivial:

def yourfunc(apple, pear, **kw):
   your_code_here

then at call time pass indict using the **kw syntax, ie:

indie = dict(pear=1, apple=2, whatever=42)
yourfunc(**indie)

No need to check anything, Python will do it by itself and raise the appropriate exception.

If you cannot change the call syntax, just wrap yourfunc with this simple decorator:

def kw2dict(func):
    def wrap(**kw):
        return func(kw)
    return wrap

(nb : should use functools to correctly wrap the decorator)

like image 2
bruno desthuilliers Avatar answered Oct 22 '22 04:10

bruno desthuilliers