Writing Python code is fun and easy. You can use Procedural programming, Functional programming , Object oriented programming or any combination. Functions are basic unit in any method you choose.

Functions are objects, defined with the ‘def’ statement followed by argument list. The function body is defined using indentation like other python statements

def fn(a,b): return a+b x = fn(10,20) 1 2 3 4 def fn ( a , b ) : return a + b x = fn ( 10 , 20 )

Arguments are named, defaults may be assigned , return statement is optional and you can return anything. Variables inside functions are local unless the keyword ‘global’ is used

Default Values

You can assign default values to the parameters:

def fn(a = 100,b = 200): return a+b print(fn()) # 100 + 200 print(fn(10)) # 10 + 200 print(fn(10,20)) # 10 + 20 1 2 3 4 5 6 7 def fn ( a = 100 , b = 200 ) : return a + b print ( fn ( ) ) # 100 + 200 print ( fn ( 10 ) ) # 10 + 200 print ( fn ( 10 , 20 ) ) # 10 + 20

if one parameter is defaulted then all that follow must also be defaulted too so def fn(a=100, b) gives a syntax error

Passing parameters by name

You can ignore the parameter position and pass it by name. This is useful when functions have many parameters with default values:

def fn(a = 100,b = 200): return a+b*10 print(fn(a=10,b=20)) # 210 print(fn(b=30,a= 10)) # 310 1 2 3 4 5 6 def fn ( a = 100 , b = 200 ) : return a + b * 10 print ( fn ( a = 10 , b = 20 ) ) # 210 print ( fn ( b = 30 , a = 10 ) ) # 310

Enforcing Named Parameters (Python 3)

You can use * to force a user to supply named arguments:

def fn(*, a = 100,b = 200): return a+b*10 print(fn()) # 2100 print(fn(b=30,a= 10)) # 310 print(fn(10,20)) # TypeError fn() takes 0 positional arguments but 2 were given 1 2 3 4 5 6 7 def fn ( * , a = 100 , b = 200 ) : return a + b * 10 print ( fn ( ) ) # 2100 print ( fn ( b = 30 , a = 10 ) ) # 310 print ( fn ( 10 , 20 ) ) # TypeError fn() takes 0 positional arguments but 2 were given

Variadic Functions

Variadic functions have a variable number of parameters. They can be collected into a tuple with a * prefix

def fn(a ,*all ): sum=0; for item in all: sum+=item return a+sum print(fn(10)) # 10 print(fn(10,20)) # 30 print(fn(10,20,30,40,50)) # 150 1 2 3 4 5 6 7 8 9 10 def fn ( a , * all ) : sum = 0 ; for item in all : sum += item return a + sum print ( fn ( 10 ) ) # 10 print ( fn ( 10 , 20 ) ) # 30 print ( fn ( 10 , 20 , 30 , 40 , 50 ) ) # 150

Keyword Parameters

Keyword parameters (kwargs) enable the parameters to be passed as a disctionary. The parameters are placed into a dictionary means that the user does not need to get the order correct. This is useful with a long parameter list for example while connecting to a database (user, password, dbname, …) . A kwargs parameter may only be used at the end of a parameter list.

You can send named parameters or a dictionary. Use ** if caller’s parameters are in a dictionary

def fn(**kwargs ): return kwargs['a'] + kwargs['b'] print(fn(a=200,b=500)) # 700 d1 = {'a':100 , 'b':200} print(fn(**d1)) # 300 1 2 3 4 5 6 7 def fn ( * * kwargs ) : return kwargs [ 'a' ] + kwargs [ 'b' ] print ( fn ( a = 200 , b = 500 ) ) # 700 d1 = { 'a' : 100 , 'b' : 200 } print ( fn ( * * d1 ) ) # 300

Global Variables

The rules of scope in Python are that an undefined variable used in a function must be a global, but if a value is assigned in the function before it is used then it is a local.

For example:

num = 9 def f1(): num = 20 def f2(): print(num) f2() # 9 f1() f2() # 9 1 2 3 4 5 6 7 8 9 10 11 num = 9 def f1 ( ) : num = 20 def f2 ( ) : print ( num ) f2 ( ) # 9 f1 ( ) f2 ( ) # 9

The function f1 declares a local copy of num so it won’t change the global value. To access the global num:

num = 9 def f1(): global num num = 20 def f2(): print(num) f2() # 9 f1() f2() # 20 1 2 3 4 5 6 7 8 9 10 11 12 num = 9 def f1 ( ) : global num num = 20 def f2 ( ) : print ( num ) f2 ( ) # 9 f1 ( ) f2 ( ) # 20

Nested Functions

You can place a function inside another function for simple scope or for closure

In the following example only f1 can use the function f2.

def f1(a): b=100 def f2(s): print(b) # 100 return s+10 c=f2(a) return c print(f1(1000)) f2(66) # NameError: name 'f2' is not defined 1 2 3 4 5 6 7 8 9 10 def f1 ( a ) : b = 100 def f2 ( s ) : print ( b ) # 100 return s + 10 c = f2 ( a ) return c print ( f1 ( 1000 ) ) f2 ( 66 ) # NameError: name 'f2' is not defined

variables in nested functions:

If we define a variables with the same name in a different scopes it may confuses the interpreter for example:

b = 3 def f1(): b = 10 def f2(): if b < 100: # error here b += 1 f2() print(b, "from f1") f1() print(b, "from main") 1 2 3 4 5 6 7 8 9 10 11 12 13 b = 3 def f1 ( ) : b = 10 def f2 ( ) : if b < 100 : # error here b += 1 f2 ( ) print ( b , "from f1" ) f1 ( ) print ( b , "from main" )

This code will fail with the error: UnboundLocalError: local variable ‘b’ referenced before assignment

To fix this you need to declare b as global or nonlocal

nonlocal

b = 3 def f1(): b = 10 def f2(): nonlocal b if b < 100: b += 1 f2() print(b, "from f2") # 11 from f2 f1() print(b, "from main") # 3 from main 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 b = 3 def f1 ( ) : b = 10 def f2 ( ) : nonlocal b if b < 100 : b += 1 f2 ( ) print ( b , "from f2" ) # 11 from f2 f1 ( ) print ( b , "from main" ) # 3 from main

global

b = 3 def f1(): b = 10 def f2(): global b if b < 100: b += 1 f2() print(b, "from f2") # 10 from f2 f1() print(b, "from main") # 4 from main 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 b = 3 def f1 ( ) : b = 10 def f2 ( ) : global b if b < 100 : b += 1 f2 ( ) print ( b , "from f2" ) # 10 from f2 f1 ( ) print ( b , "from main" ) # 4 from main

Closure

Functions are objects so you can pass a function as a parameter to another function or return a function from another function. This is useful when you write a function that create and return another function dynamically for example, gets a list of points (x,y) and returns an interpolation function.

Example:

def getmulby(m): def op(n): return m*n return op f1=getmulby(10) f2=getmulby(5) print( f1(2) ) # 20 print( f2(2) ) # 10 1 2 3 4 5 6 7 8 9 10 def getmulby ( m ) : def op ( n ) : return m * n return op f1 = getmulby ( 10 ) f2 = getmulby ( 5 ) print ( f1 ( 2 ) ) # 20 print ( f2 ( 2 ) ) # 10

Each call to getmulby returns a new function

Decorator

Decorator is a good example of closure. Decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it

def add_stars(some_function): def wrapper(): print("********************") some_function() print("********************") return wrapper @add_stars def my_function(): print("Hello!!!") my_function() # ******************** # Hello!!! # ******************** 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def add_stars ( some_function ) : def wrapper ( ) : print ( "********************" ) some_function ( ) print ( "********************" ) return wrapper @ add_stars def my_function ( ) : print ( "Hello!!!" ) my_function ( ) # ******************** # Hello!!! # ********************

Before calling my_function we are actually calling add_stars sending my_function as a parameter. The function add_stars returns the inner function (closure) and this is what we really invoke.

This is very helpful while writing infrastructures

Lambda Expressions

lambda functions are anonymous functions for simple operations. It is very common where we pass a function to another function. For example the builtin map function to map a list to a new list:

ls = [2,4,6] newlist = map(lambda item:item * 2, ls) for n in newlist: print(n) # 4 # 8 # 12 1 2 3 4 5 6 7 8 9 ls = [ 2 , 4 , 6 ] newlist = map ( lambda item : item * 2 , ls ) for n in newlist : print ( n ) # 4 # 8 # 12

lambda functions cannot contain branches or loops but can contain conditional expression. Usually a simple expression:

import random f1 = lambda x:x+10 f2 = lambda :random.randint(100,200) f3 = lambda x,y:x+y compare=lambda a,b: -1 if a < b else (+1 if a > b else 0) print( f1(10) ) # 20 print( f2() ) # prints random number print( f3(2,3)) # 5 print( compare(10,20) ) # -1 1 2 3 4 5 6 7 8 9 10 11 import random f1 = lambda x : x + 10 f2 = lambda : random . randint ( 100 , 200 ) f3 = lambda x , y : x + y compare = lambda a , b : - 1 if a < b else ( + 1 if a > b else 0 ) print ( f1 ( 10 ) ) # 20 print ( f2 ( ) ) # prints random number print ( f3 ( 2 , 3 ) ) # 5 print ( compare ( 10 , 20 ) ) # -1

Function Documentation

You can add docstring comments to a function. It is used for help function and automated testing.

def add(a,b): """ Simple function - add 2 numbers""" return a+b >>> help(add) Help on function add in module __main__: add(a, b) Simple function - add 2 numbers 1 2 3 4 5 6 7 8 9 def add ( a , b ) : """ Simple function - add 2 numbers""" return a + b >>> help ( add ) Help on function add in module __main__ : add ( a , b ) Simple function - add 2 numbers

It is stored in an attribute __doc__. You can set it explicitly

Function Annotations (Python 3)

You can add documentation to the parameters and the return value:

def add(a:"first number" = 0, b:"second number" = 0) -> "sum of a and b": return a+b for item in add.__annotations__.items(): print(item) # ('a', 'first number') # ('b', 'second number') # ('return', 'sum of a and b') 1 2 3 4 5 6 7 8 9 10 def add ( a : "first number" = 0 , b : "second number" = 0 ) -> "sum of a and b" : return a + b for item in add . __annotations__ . items ( ) : print ( item ) # ('a', 'first number') # ('b', 'second number') # ('return', 'sum of a and b')

Subscribe to our list