6

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)):
Jan
  • 4,932
  • 1
  • 26
  • 30

8 Answers8

7

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:
    ...
mgilson
  • 300,191
  • 65
  • 633
  • 696
  • Is it possible to define the type for a key? ie: `properties: dict(name: str=None, age: int=None)` – Stevoisiak Feb 28 '18 at 20:13
  • 1
    @StevenVascellaro -- This answer predates mypy. These days, perhaps using a mypy `TypedDict` would make for a reasonable solution: https://github.com/python/mypy/blob/master/extensions/mypy_extensions.py – mgilson Feb 28 '18 at 20:21
  • Could you perhaps explain further in an edit or separate answer? – Stevoisiak Feb 28 '18 at 20:22
  • @StevenVascellaro -- Sure. I've added a excerpt of completely untested code :-) – mgilson Feb 28 '18 at 20:27
  • 1
    also see [PEP 589 -- TypedDict](https://www.python.org/dev/peps/pep-0589/) – djvg Apr 09 '21 at 14:15
3

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
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
2

Another option is to use a decorator:

@required(indict=('apple','pear'))
def fun(indict=None):
    print 'parameters are okay'

The somewhat complex decorator:

from functools import wraps

def required(**mandatory):
    def decorator(f):
        @wraps(f)
        def wrapper(**dicts):
            for argname,d in dicts.items():
                for key in mandatory.get(argname,[]):
                    if key not in d:
                        raise Exception('Key "%s" is missing from argument "%s"' % (
                            key,argname))
            return f(**dicts)
        return wrapper
    return decorator

Examples:

>>> fun(indict={})
Traceback (most recent call last):
  ...
Exception: Key "apple" is missing from argument "indict"

>>> fun(indict={'apple':1})
Traceback (most recent call last):
  ...
Exception: Key "pear" is missing from argument "indict"

>>> fun(indict={'apple':1, 'pear':1})
parameters are okay
Norbert Sebők
  • 1,208
  • 8
  • 13
2

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)

bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118
1

One option would be to use keyword arguments and then dictionary expansion:

def fun(apple=None, pear=None, **unused_kwargs):
    # ... do stuff with apple and pear

and then when calling it...

fun(**arguments_dict)

This will automatically pull out the values for the "apple" and "pear" keys into variables, and leave everything else in a dictionary called unused_kwargs.


However, this still doesn't require the apple and pear keys to be present in and of itself - they'll just use the default values provided if left out. You could add checks for this:

def fun(apple=None, pear=None, **unused_kwargs):
    if apple is None or pear is None:
        raise ValueError("Missing one or more required arguments.")
Amber
  • 507,862
  • 82
  • 626
  • 550
  • But that still doesn't require the caller to specify a value for `apple` and `pear`. – Tim Pietzcker Jan 09 '14 at 08:12
  • 3
    Why not use `fun(apple, pear, **unused_kwargs)` and let python ensure these keys are in the dict? – Cilyan Jan 09 '14 at 08:19
  • This is a good option. The only, thing is that if an argument is not present, it only says 'func() takes exactly 2 arguments (1 given)'. So it looks like, one then has to find the missing arguments by itself. – Jan Jan 09 '14 at 08:40
1

Please, consider the "duck typing" topic in Python (see best answer in How to handle "duck typing" in Python?)

Generaly there are several ways how to aproach this kind of problem: First: missing keys in indict are fatal to your code: then let it raise an exception.

# Trust there are proper data in indict
def fun(indict):
    # do something with indict
    return indict["apple"]

fun({})
>> KeyError: 'apple'

Second: missing keys in indict are not fatal: then log the error, do what it take to workaround / recover, and continue.

# Try to recover from error
def fun(indict):
    if 'apple' not in indict:
        logging.warning('dict without apples in it.')
        return None
    return indict['apple']

fun({})
>> None

And so on ...

What you should consider in first place is your application and the speed performances you need from it.

Try to answer this for yourself:

  • why do you want this kind of check for?
  • who should solve problematic usage of this function (function's owner, function's user)?
  • many ifs, typechecks etc (which would be there in every call of your func) may cause unnecessary overheads. Is it OK to you?
  • what is a proper way to your app to respond for this "crappy" data? (crash?)
Community
  • 1
  • 1
Petr Joachim
  • 3,970
  • 1
  • 13
  • 9
0

You can add a check function:

def fun(indict):
    check(indict, ('apple','pear'))
    print 'parameters are okay'

def check(d, keys):
    for k in keys:
        if not k in d:
            raise Exception("NeedsToBeSpecified: %s" % k)

It raises exception if a required key is missing from the dict:

>>> fun({})
Traceback (most recent call last):
  ...
Exception: NeedsToBeSpecified: apple

>>> fun({'apple':1})
Traceback (most recent call last):
  ...
Exception: NeedsToBeSpecified: pear

>>> fun({'apple':1, 'pear':1})
parameters are okay
Norbert Sebők
  • 1,208
  • 8
  • 13
0

Rather than reinvent the wheel, you could use the predefinedpreconditiondecorator in the PythonDecoratorLibrary something like this:

# see https://wiki.python.org/moin/PythonDecoratorLibrary#Pre-.2FPost-Conditions
from pre_post_conditions import precondition

def indict_contains_keys(required):
    def checker(inval):
        assert isinstance(inval, dict)
        for key in required:
            if key not in inval:
                raise AssertionError('indict has no key %r' % key)
    return checker

@precondition(indict_contains_keys(['apple', 'pear']))
def fun(indict):
    print 'fun({!r} OK'.format(indict)

fun({'apple':1, 'pear': 2})           # fun({'pear': 2, 'apple': 1} OK
fun({'apple':1, 'pear': 2, 'fig': 3}) # fun({'pear': 2, 'apple': 1, 'fig': 3} OK
fun({'apple':1, 'fig': 3})            # AssertionError: indict has no key 'pear'
martineau
  • 119,623
  • 25
  • 170
  • 301