Python Notes and Examples

Functions

When you define a function, you of course have to list all the parameters. Any params with default values must come after those without.

When you call a function, you may — if you like — supply your arguments as named arguments, regardless of whether or not the param had a default value when the function was defined. (These are sometimes called “keyword params/args”, reminiscent of “key/value pairs in a dict”).

# One positional parameter.
def foo(n):
    print(n)

foo(5)   # Yup
foo(n=5) # Calling with named argument is fine too.
foo(x=5) # Error

# Param with default value (a named param).
def bar(n=3):
    print(n)

bar(5)   # Yup
bar(n=5) # Fine too
bar()    # Fine too, uses default value
bar(x=5) # Error

If your args supply all param names (that is, if you call the function using named args), you can pass in the named args in any order you like.

Note that if you supply mutable default values in your function parameters, they are only evaluated once — when the function is defined — so they will persist between calls:

Varargs

If a function is defined like def foo(a, b, *foo, **bar), then any extra positional args passed to it get soaked up into a tuple foo in the body of the function, and any extra named args (of course, not a or b though, for this example) go into the bar dict.

Keyword-only params

You can specify some params be supplied only via named (“keyword”) args:

def foo(a, b, *, c, d=4):
    # both `c` and `d` are keyword-only params

def foo(a, b, *p, c, d=4):
    # both `c` and `d` are keyword-only params

def bar(*, a, b):
    # both `a` and `b` are keyword-only params

All args after the bare * must to be called using named args. In addition, of course, if you have a *p param, then any args passed after that must be keyword-only as well (otherwise they’d be positional and be soaked up by *p.

Unpacking args

When calling a function, you can unpack args by putting a * or ** in front of them:

xs = ['a', 'b', 'c']
# Function foo takes three args. So:
foo(*xs)  # Like calling `foo('a', 'b', 'c')`.

# You can unpack a string as well:
s = 'hey'
foo(*s)  # Like calling `foo('h', 'e', 'y')`.

d = {'a': 1, 'b': 2, 'c': 3}
# Function bar was defined with three params: a, b, & c.
bar(**d)  # Like calling `bar(a=1, b=2, c=3)`.

If you accidentally did bar(*d), then only the keys of d would be unpacked and passed to bar.

Note, with:

def foo(a, b, c):
    print(a, b, c)

d1 = {'a': 1, 'b': 2}
d2 = {'c': 3}
foo(**d1, **d2)

the dict keys in must match exactly with the params of foo, but you can get them from more than one dict.

Varargs, Unpacking, and Dicts

All together:

def foo(**m):
    print('-->', m)

m2 = {'a': 1, 'b': 2, 'c': 3}

foo(**m2)
# --> {'a': 1, 'c': 3, 'b': 2}

So, it’s as if m2 gets unpacked, but then re-packed into the m parameter.

Type Annotations

Python 3 introduced “function annotations”, where you could annotate functions with arbitrary strings:

def foo(a:'paul', b:'george') -> 'bagel':
    print('hi')

print(foo.__annotations__)

Python 3.6 PEP 526 introduced syntax specifically for variable type annotations.

See also MyPy for adding type annotations, and static type-checking.

Nested Functions

You can define functions inside other functions:

def foo(a, b):
    n = 0
    def bar(x, y):
        nonlocal n  # does what you expect
        n += 1
        print(a, b, n, x, y)
    bar(5, 6)
    bar(5, 6)
    return bar

f = foo(10, 11)  # 10 11 1 5 6
                 # 10 11 2 5 6

f(22, 23)        # 10 11 3 22 23
f(22, 23)        # 10 11 4 22 23