652

If I have a function like this:

def foo(name, opts={}):
  pass

And I want to add type hints to the parameters, how do I do it? The way I assumed gives me a syntax error:

def foo(name: str, opts={}: dict) -> str:
  pass

The following doesn't throw a syntax error but it doesn't seem like the intuitive way to handle this case:

def foo(name: str, opts: dict={}) -> str:
  pass

I can't find anything in the typing documentation or on a Google search.

Edit: I didn't know how default arguments worked in Python, but for the sake of this question, I will keep the examples above. In general it's much better to do the following:

def foo(name: str, opts: dict=None) -> str:
  if not opts:
    opts={}
  pass
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
josh
  • 9,656
  • 4
  • 34
  • 51

3 Answers3

692

Your second way is correct.

def foo(opts: dict = {}):
    pass

print(foo.__annotations__)

this outputs

{'opts': <class 'dict'>}

It's true that's it's not listed in PEP 484, but type hints are an application of function annotations, which are documented in PEP 3107. The syntax section makes it clear that keyword arguments works with function annotations in this way.

I strongly advise against using mutable keyword arguments. More information here.

noɥʇʎԀʎzɐɹƆ
  • 9,967
  • 2
  • 50
  • 67
  • 2
    See http://legacy.python.org/dev/peps/pep-3107/#syntax. Type hinting is just an application of function annotations. – chepner Aug 02 '16 at 18:25
  • @chepner true. didn't know that PEP 3107 had something about keyword arguments. – noɥʇʎԀʎzɐɹƆ Aug 02 '16 at 18:35
  • 4
    Wow, I didn't know about the mutable default arguments in Python... especially coming from Javascript/Ruby where default arguments work differently. Not gonna rehash what's already been said ad nauseum around SO about it, I'm just glad I found out about this before it bit me. Thanks! – josh Aug 02 '16 at 18:42
  • 46
    I was always advised to use None rather than a mutable type like {} or [] or a default object as mutations to that object without a deep-copy will persist between iterations. – MrMesees Oct 25 '18 at 10:35
  • 6
    define enough functions with mutable keyword arguments and it is only a matter of time before you find yourself looking back on a 4 hour debugging session questioning your life choices – Joseph Sheedy Jun 10 '19 at 20:50
  • 6
    Shouldn't there be no whitespace around the `=` in `dict = {}` like it is convention for non-type-hinted keyword arguments? – actual_panda Mar 24 '20 at 09:52
  • 2
    @actual_panda Not according to [PEP 8](https://peps.python.org/pep-0008/#other-recommendations:~:text=when%20combining%20an%20argument%20annotation%20with%20a%20default%20value%2C%20however%2C%20do%20use%20spaces%20around%20the%20%3D%20sign%3A), at least. – Ron Wolf Jan 09 '23 at 17:36
  • UPDATE: Since python v3.10, you can use: ``def foo(arg: dict | None = None) -> dict | None:`` See: https://docs.python.org/3.10/library/typing.html#typing.Union – Geronimo Aug 17 '23 at 07:15
106

If you're using typing (introduced in Python 3.5) you can use typing.Optional, where Optional[X] is equivalent to Union[X, None]. It is used to signal that the explicit value of None is allowed . From typing.Optional:

def foo(arg: Optional[int] = None) -> None:
    ...
Tomasz Bartkowiak
  • 12,154
  • 4
  • 57
  • 62
  • 4
    Shouldn't there be no whitespace around the `=` in `Optional[int] = None` like it is convention for non-type-hinted keyword arguments? – actual_panda Mar 24 '20 at 09:52
  • 18
    @actual_panda the answer is correct. the style is different when there are type hints. there are examples in [PEP 484](https://www.python.org/dev/peps/pep-0484/) – joel Jul 03 '20 at 11:05
  • @actual_panda Not according to [PEP 8](https://peps.python.org/pep-0008/#other-recommendations:~:text=when%20combining,%2E%2E%2E,-compound). Also, if you're going to post a comment twice on the same page, you should consider asking it as its own question. As noted in the [help page](https://stackoverflow.com/help/privileges/comment#:~:text=when%20shouldn%27t,the%20following%3A&text=secondary%20discussion,chat%20instead%3B) for the `comment everywhere` privilege, “Comments are not recommended for … Secondary discussion” – Ron Wolf Jan 09 '23 at 18:13
  • 1
    or `def foo(arg: int | None = None) -> None:` in the newer syntax – joel May 18 '23 at 13:54
38

I recently saw this one-liner:

def foo(name: str, opts: dict=None) -> str:
    if opts is None:
        opts = {}
    pass  # ...
ojdo
  • 8,280
  • 5
  • 37
  • 60
Kirkalicious
  • 397
  • 3
  • 4
  • Hi @Kirkalicious, thanks for your answer. Could you explain how it works? – Nathan Jun 05 '19 at 21:09
  • 22
    The empty dict passed as a default parameter is the same dict for every call. So if the function mutates it then the default next time will be the mutated value from last time. Making `None` the default and then checking inside the method avoids this problem by allocating a new dict each time the method is invoked. – Ian Goldby Jun 21 '19 at 07:36
  • 2
    Can you [update your answer](https://stackoverflow.com/posts/56467744/edit) (***without*** "Edit:", "Update:", or similar)? Comments may disappear at any time. – Peter Mortensen Jul 14 '20 at 15:26
  • 9
    How about `opts = opts or {}` – run_the_race Sep 19 '20 at 15:49
  • 6
    One issue with this -- If *opts* is a mutable parameter that callers will want to see modified values in, it will fail when `{}` is passed in. Probably safer to stick with the traditional two-liner `if opts is None: opts = {}`. If you need a one-liner, I'd prefer `opts = {} if opts is None else opts`. – Gavin S. Yancey Jul 22 '21 at 17:22
  • @GavinS.Yancey This is true when your function is mutating, but unnecessary otherwise. Given `opts or `if/then/none` is a particularly disorganized construct, and I prefer explicit, functional behavior over mutation, I find `value or default` clearer 9/10 times. – Kevin McDonough Oct 26 '22 at 18:22