How to structure your code

🤔 How to get rid of spaghetti code?

Use functions and modules!

Python functions

Function syntax

Function syntax

💡 Functions are the key mechanism to structure your code.

Selected topics

  • Default arguments
  • Keyword arguments
  • Arbitrary arguments
  • Anonymous functions
  • Call by reference
Function syntax

Default arguments

  • Can be used to define a default behavior of a function
  • Default arguments must appear after non-default arguments
def add(x, y=0):
    return x + y

add(1, 2)   # == 3
add(1)      # == 1
Function syntax

Keyword arguments

  • Caller can pass arguments by name
  • Argument ordering can be changed
  • Positional arguments must not follow keyword arguments
def power(x, y):
    return x**y

power(2, 3)       # == 8
power(3, 2)       # == 9
power(x=2, y=3)   # == 8
power(y=3, x=2)   # == 8
power(2, y=3)     # == 8
Function syntax

Keyword arguments

  • You can force the caller to use keyword arguments
  • Supports the Open-Closed principle, because the function signature can be changed
def power(*, x, y):
    return x**y

power(x=2, y=3)   # == 8
power(2, 3)       # TypeError: power() takes 0 positional arguments but 2 were given
Function syntax

Arbitrary arguments

  • *args will collect all remaining positional arguments as a tuple
  • **kwargs will collect all remaining keyword arguments as a dictionary
def test(x, y, *args, **kwargs):
    print(x, y, args, kwargs)

test(1, 2)            # 1 2 () {}
test(1, 2, 3)         # 1 2 (3,) {}
test(x=1, y=2, z=3)   # 1 2 () {'z': 3}
test(1, 2, 3, z=4)    # 1 2 (3,) {'z': 4}
Function syntax

Arbitrary arguments

  • Often used to support the Open-Closed principle
def add(x, y):
    return x + y

def squared_sum(*args):
    return add(*args)**2

squared_sum(1, 2)   # == 9
Function syntax

Anonymous functions

  • Use lambda expression to define a function
add = lambda x, y : x + y
add(1, 2)   # == 3
  • Possible use case: functionals
def power(n):
    return lambda x : x**n

power3 = power(3)
power3(2)   # == 8
Function syntax

Call by reference

  • Arguments are passed by reference in Python
  • This makes it possible to change values without a return statement
  • ⚠️ Never use this approach! It violates the SOC principle!
def zero(x):
    # Don't do this!
    x[0] = 0

numbers = [1, 2]
zero(numbers)
print(numbers)   # [0, 2]
Type hints

Type hints

Python code is interpreted, not precompiled. Variables can change their type at runtime.

➕ Flexibility to reuse code and reduce complexity

def add(x, y):
    return x + y

add(1, 2)
add('foo', 'bar')

➖ Bugs can occur where variables are assigned accidentally

number = 0
number = 'zero'
Type hints

💡 Type hints can be used to enable type checking with mypy.

This will be executed by the Python interpreter without errors:

number = 0  # type: int
number = 'zero'

Only mypy will complain:

test.py:2: error: Incompatible types in assignment (expression has type "str", variable has type "int")  [assignment]
Found 1 error in 1 file (checked 1 source file)
Type hints

Type hints can be used to check function arguments and return values:

def add(x : int, y : int) -> int:
    return x + y

add(1, 2)
add(1.0, 2.0)

Error message from mypy:

$> mypy add.py
add.py:5: error: Argument 1 to "add" has incompatible type "float"; expected "int"  [arg-type]
add.py:5: error: Argument 2 to "add" has incompatible type "float"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)
Type hints

Renaming a type hint is also possible:

Number = int
def add(x : Number, y : Number) -> Number:
    return x + y
Type hints

🤔 Should I add type hints to my entire code base?

Probably not.

✅ Incrementally add type hints to

  • detect bugs with static code analysis by mypy
  • document the input and output of certain key functions

See PEP 484 for all details.

Decorators

Decorators

💡 Decorators can be used to modify the input and/or output of a function.

  • Built-in decorators exist, for example: @staticmethod and @classmethod
  • Own decorators can be created:
from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):

        # before the function call

        result = func(*args, **kwargs)

        # after the function call

        return result
    return wrapper
Decorators

Example of a useful decorator

It prints the arguments of a function and its return values for debugging:

from functools import wraps

def debug(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('Function {} called with args {} and kwargs {}'.format(func.__name__, args, kwargs))
        result = func(*args, **kwargs)
        print('Function {} returned {}'.format(func.__name__, result))
        return result
    return wrapper

@debug
def add(x, y):
    return x + y

add(1, 2)
Function add called with args (1, 2) and kwargs {}
Function add returned 3
Further reading

Further reading

https://www.geeksforgeeks.org/python-functions/
https://www.w3schools.com/python/python_functions.asp