EuroPython 2018: More Than You Ever Wanted To Know About Python Functions

Mark Smith has been a Python developer & trainer for 18 years and is now trying out Developer Relations to see how that feels.

Functions are normally taught early on, because curricula want to go through with the basics fast, so the details get lost at first, and sometimes you never catch up with them.

Simple functions

Let's look at a simple function without parameters and without return values. The first line, defining the function is usually only executed once in the lifetime of the program. Functions are regular variables, and you can assign them regularly to other variables.

Functions with parameters

You can call a function with several parameters in many different ways:

def send(recipient, message):  # parameters
    print(f'{recipient}: {message}')

# call with positional arguments
send('foo', 'bar')

# call with named arguments (commonly called keyword arguments, only they're not keywords)
send(message='bar', recipient='foo')

# a mix, raising a TypeError!
send('foo', recipient='bar')

Let's called them "named arguments" instead of "keyword arguments", as they're just not keywords. They're very practical, as you don't have to memorize order! Be careful mixing named and positional arguments, if you accidentally overload one of the arguments, you'll get a TypeError.

Functions with default values

You can also provide default arguments, which have to be defined after positional arguments.

def send(recipient, message, sender='me'):
    print(f'{recipient}: {message}')

# call with positional arguments, leave or set default
send('foo', 'bar')
send('foo', 'bar', 'not me')

# call with named arguments
send(message='bar', recipient='foo', sender='not me')

Beware of mutable default arguments! Don't set def foo(argument1=[]) - lists are mutable, and if you now go ahead and alter argument1 in the method, it will persist across method calls (as the definition line is only executed once). Use instead None, and check if the value is None, set the variable to the default.

Beware of passing in mutable values to functions, and then just using them in that function, because you'll find yourself in side effect hell. So don't use mutating functions on mutable objects you receive, but rather create new objects ([] + ['foo'] instead of append).

Variadic parameters and "keyword" arguments

def printf(fmt, *args):
  pass

def printf(fmt, **args):
  pass

This soaks up all remaining unnamed arguments. You can't provide this explicitly as a named tuple. You also can't pass in a dictionary directly. You can combine the two of them. Make sure that *args is the last positional argument.

Calling methods

So generally you want to use keyword arguments to call methods, as you're future proof, and more readable. If you want ot force people to use keyword arguments, use the * shortcut (which you can also stick in the middle or your parameters):

def send(*, message=None, recipient=None):
  pass

Sadly, in Python unpacking arguments looks very similar to the parameter syntax, so with longer or optional arguments, you may want to do:

args = ('1', 2, 3)
kwargs = {'foo': 34}

call_method(*args, **kwargs)

Also, keep in mind that None is returned if you don't return something explicitly.

Scopes

There were three scopes: functions, modules, and builtins, searched in this order. If you use a non-defined variable, which is present in the module scope, that variable will be used.

This makes assigning to global variables tricky, because instead you'll find yourself with a local variable instead. Premature access to such a variable may also crash your program in horrible wasy (UnboundLocalError). Use the global declaration to make explicit that you use a global variable (it's not really about global variables, you just go from functions to module scope).

Three scopes are the past though – by now we have inner functions, as you can define functions within functions. Assigning to outer function scoped variables, use nonlocal. Both nonlocal and global should be use with extreme caution, and you should reconsider your structure in most places before using though.

Classes

Methods on classes look very much like a function. Their first parameter will never be explicitly passed. Class.do_foo is a function, but instance.do_foo is a bound method. (Fun fact! You can use __class__, not self.__class__ to figure out what class you're in, starting in Python 3. This is how the fancy short super() works.)

If a class defines a __get__, this will used to access the method. (This is why you don't need to pass self in.)

Callable objects

If you define __call__ on objects, suddenly they're callable.

Inspecting functions

Use inspect.signature to look how the function signature looks like. Look at enforce which enforces types at runtime using this.

Function attributes

Functions have __code__, exposing bytecode. So this is where you go to do weird hackery.