Skip to content

Functions

Docstring 📝

Junior Coder Docstring Style

[cite_start]A simple, descriptive docstring[cite: 147]. Note that the is_even function must be defined with this docstring for the subsequent __doc__ example to work correctly.

def is_even (num):
  """
  This function returns if a given number is even or odd.
  input - any valid integer
  output - boolean (True if even, False if odd)
  created on - Apr 2 2025
  """
  if type(num) == int :
    if num % 2 == 0:
      return 'Even'
    else:
      return 'Odd'
  else:
    return 'Read docstring ... go...'
Output

Senior Coder Docstring Style (Using Type Hinting and Standard Format)

[cite_start]A more structured docstring using type hints (num: int -> str) and a formal format (like NumPy or Google style), listing arguments and return values[cite: 147].

def is_even(num: int) -> str :
    """
    Check given number is even or odd.

    Args:
        num (int): Any Valid Integer

    Returns:
        str: Even or Odd

    """
    if type(num) == int : # Function body added to make it callable
      if num % 2 == 0:
        return 'Even'
      else:
        return 'Odd'
    else:
      return 'Read docstring ... go...'
Output

Calling the Function

Assuming the function definition from the first example is active in the environment.

1
2
3
4
# call the function

for i in range (0, 14):
  print (i, is_even(i))
Output
0 Even
1 Odd
2 Even
3 Odd
4 Even
5 Odd
6 Even
7 Odd
8 Even
9 Odd
10 Even
11 Odd
12 Even
13 Odd


Reading Docstrings (.__doc__)

Custom Docstring

[cite_start]Access the docstring using the special .__doc__ attribute[cite: 147].

print(is_even.__doc__)
Output
1
2
3
4
 This function returns if a given number is even or odd.
 input - any valid integer
 output - boolean (True if even, False if odd)
 created on - Apr 2 2025

Docstring of Built-in print()

Built-in functions and objects also have docstrings.

print(print.__doc__)
Output
Prints the values to a stream, or to sys.stdout by default.

 sep
   string inserted between values, default a space.
 end
   string appended after the last value, default a newline.
 file
   a file-like object (stream); defaults to the current sys.stdout.
 flush
   whether to forcibly flush the stream.

Docstring of Built-in type()

print(type.__doc__)
Output
type(object) -> the object's type
type(name, bases, dict, **kwds) -> a new type


Two Points of View: Responsibility

View

Whenever you design a function, it is your responsibility to ensure  that your code executes without any errors.

  • Junior Coder: Focuses on writing the logic to pass the test cases.
  • Senior Coder: Writes robust functions that include type checking (like in the first is_even example) and clear documentation (docstrings) to prevent misuse and runtime errors, reflecting code quality and reliability.

Parameter vs Arguments 🤝

Definitions

  • Parameters are the names defined in the function signature (during function creation time).
  • Arguments are the actual values passed to the function when it is called (during function use time).

1
2
3
# def func_name(parameter1, parameter2):  <- Parameters
#     pass
# func_name(argument1, argument2)        <- Arguments
Output


Default Argument

Default Argument

Parameters can be assigned a default value in the function definition. If no argument is provided for that parameter during the call, the default value is used.

1
2
3
4
5
6
7
# default
# if no value is provided, the default value (1) is used

def power (a=1, b=1):
  return a**b

power()
Output
1

Positional Argument

Positional Argument

Arguments passed in the function call are mapped to parameters based on their position or order.

1
2
3
4
# positional argument
# order matters: 2 is assigned to 'a', 3 is assigned to 'b'

power(2, 3)
Output
8

Keyword Argument

Keyword Argument

Arguments are passed by explicitly naming the parameter they should correspond to. This allows the arguments to be specified in any order.

1
2
3
4
# keyword
# arguments are matched by name, ignoring position

power(b=3, a=2)
Output
8


*args & **kwargs 📦

The special Python keywords *args and **kwargs are used to pass a variable length of arguments to a function.

Variable Positional Arguments (*args)

The *args syntax allows you to pass a variable number of non-keyword arguments (positional arguments) to a function. Inside the function, args (or whatever name you choose) is treated as a tuple containing all the passed non-keyword arguments.

# *args (keyword 'kwargs' is used instead of convention 'args' to demonstrate flexibility)
# allows us to pass a variable number of non-keyword arguments to a function.

def multiply(*kwargs):
  product = 1

  for i in kwargs:
    product = product * i

  print(kwargs) # Prints the tuple of arguments
  return product
Output

multiply(1, 2, 3, 4, 5, 6, 7, 8, 9)
Output
(1, 2, 3, 4, 5, 6, 7, 8, 9)
362880

Variable Keyword Arguments (**kwargs)

The **kwargs syntax allows you to pass any number of keyword arguments (key-value pairs) to a function. Inside the function, kwargs (or whatever name you choose) is treated as a dictionary containing all the passed keyword arguments.

1
2
3
4
5
6
7
# **kwargs
# **kwargs allows us to pass any number of keyword arguments.
# Keyword arguments mean that they contain a key-value pair, like a Python dictionary.

def display(**kwargs):
  for (key,value) in kwargs.items():
    print(key,'->',value)
Output

display(a=1, b=2, c=3)
Output
1
2
3
a -> 1
b -> 2
c -> 3


Important Points to Remember
  1. Order Matters: If you use a combination of argument types, the order in the function signature must be: $$ \text{normal arguments} \rightarrow \text{positional args } (\text{args}) \rightarrow \text{keyword args } (*\text{kwargs}) $$

  2. Naming Convention: The words "args" and "kwargs" are just a convention (standard practice). You can use any valid variable names (e.g., *elements, **attributes), but stick to the convention for better readability.


Functions Without a return Statement 🔄

Implicit Return Value: None

If a function or method executes without an explicit return statement, it implicitly returns the value None. Many list methods, like .append(), modify the list in place and return None.

1
2
3
l = [1, 2, 3]
print(l.append(5)) # The append method modifies l, but returns None
print(l)
Output
None
[1, 2, 3, 5]


Variable Scope 🌐

  • In Python, variables are resolved based on the LEGB rule (Local, Enclosing, Global, Built-in).

Global vs. Local Variables

A function can access (read) a variable defined in the Global scope if no local variable with the same name exists.

1
2
3
4
5
6
7
def g(y):
  print(x)
  print(x+1)

x=5 # Global variable
g(x)
print(x)
Output
1
2
3
5
6
5

When a variable is assigned a value inside a function, it becomes a Local variable, which takes precedence over the global variable with the same name. It does not affect the global variable.

1
2
3
4
5
6
7
8
def f(y):
  x=1 # x is local to f
  x+=1
  print(x)

x=5 # Global variable
f(x)
print(x) # Global x remains unchanged
Output
2
5

When an immutable argument (x=3) is passed to a function, the function receives a copy of the reference (pass-by-object-reference). Operations that modify the variable (like x = x + 1) create a new local variable x inside the function's scope, leaving the global x untouched.

1
2
3
4
5
6
7
8
9
def f(x):
   x = x + 1 # Creates new local x
   print('in f(x): x =', x)
   return x

x = 3 # Global x
z = f(x)
print('in main program scope: z =', z)
print('in main program scope: x =', x) # Global x is still 3
Output
1
2
3
in f(x): x = 4
in main program scope: z = 4
in main program scope: x = 3

The global keyword declares that a variable inside the function should refer to the global one. You cannot use global on a variable that is already defined as a function parameter, as parameters are always local to the function.

1
2
3
4
5
6
7
8
9
# we can't change the global function

def h(x): # x is already a local parameter
    global x # Error: cannot redeclare parameter as global
    x+=1

x=5
h(x)
print(x)
Output
1
2
3
global x
    ^
SyntaxError: name 'x' is parameter and global


Nested Functions 🪜

Nested Function Execution

A function defined inside another function (a nested function) can only be called from within its enclosing function.

1
2
3
4
5
def f():
  def g(): # g is local to f
    print('Inside g')
  g() # g() is called inside f
  print('Inside f')
Output

Calling the outer function executes it, which in turn calls the inner function.

f()
Output
Inside g
Inside f

A variable assignment (x = 'abc') inside the inner function (h) creates a local variable x within h. This new x does not affect the x in the enclosing function (g).

def g(x):
    def h():
        x = 'abc' # x is local to h
    x = x + 1 # x is local to g (from parameter)
    print('in g(x): x =', x)
    h() # h is executed but changes only its local x, which is immediately discarded
    return x

x = 3 # Global x
z = g(x)
Output
in g(x): x = 4

When the inner function (h) also takes a parameter named x, it has its own separate local scope. Modifications to x inside h do not affect the x in the enclosing function (g) or the global x.

def g(x): # x is local to g (4)
    def h(x): # x is local to h (5)
        x = x+1
        print("in h(x): x = ", x)
    x = x + 1
    print('in g(x): x = ', x)
    h(x)
    return x

x = 3 # Global x (3)
z = g(x)
print('in main program scope: x = ', x)
print('in main program scope: z = ', z)
Output
1
2
3
4
in g(x): x = 4
in h(x): x = 5
in main program scope: x = 3
in main program scope: z = 4


Functions as First-Class Citizens 🥇

In Python, functions are considered first-class citizens, meaning they possess the following properties, similar to data types like integers or strings:

  • Functions are a data type (<class 'function'>).
  • They can be assigned to variables.
  • They can be stored in data structures (lists, tuples, sets, dictionaries).
  • They can be passed as arguments to other functions.
  • They can be returned as values from other functions.

Properties of Functions as Data

A function is a distinct type, and like any object, it has a unique memory address (id).

1
2
3
4
5
6
7
# type and id

def square(num):
  return num**2

print(type(square))
print(id(square))
Output
<class 'function'>
1398082569376

The function object can be assigned to a new variable (x). Both names point to the same object (same ID).

1
2
3
4
5
6
# reassign

x = square
print(id(x))
print(id(square))
print(x(5))
Output
1
2
3
1398082569376
1398082569376
25

You can delete the original name, but the function object remains if it is referenced by another name (like x in the previous step). The function must be redefined to use the original name again.

1
2
3
# deleting a function

del square
Output

def square(num):
  return num**2
Output

Function objects can be stored inside lists, tuples, sets, or dictionaries.

1
2
3
4
# storing in a list

l = [1, 2, 3, square]
print(l)
Output
[1, 2, 3, <function square at 0x00000233B2713420>]

Functions are considered immutable/hashable, allowing them to be stored in sets.

1
2
3
4
# storing in a set

s = {square}
print(s)
Output
{<function square at 0x00000233B2713420>}

A function (f) can define and return another function (x), which can then be called immediately.

# returning a function

def f():
  def x(a, b):
    return a+b
  return x

# f() returns x; (3, 4) calls x
val = f()(3, 4) 
print(val)
Output
7

A function (func_c) can accept another function (func_a) as an argument and execute it.

# function as argument

def func_a():
  print('inside func_a')

def func_c(z): # z accepts a function
  print('inside func_c')
  return z() # executes the function passed as z

print(func_c(func_a))
Output
1
2
3
inside func_c
inside func_a
None