1. Basics: variables, a bit of math and console input/output

  • print writes strings and values

  • separate arguments with ,

print(1, 2, 3) 
print(4)
print("done")
1 2 3
4
done

One line comments:

# this is a comment
a = 3 # and this is another comment

No declaration of variable types, just assign values. Type of variable is determined from value on the right side of =:

a = 1.23
print(a * a)
1.5129

Check type of a variable with built in type function:

print(type(a))
<class 'float'>
b = 4711 * 42
print(type(b))
<class 'int'>
c = "I heart Python"
print(type(c))
<class 'str'>

Valid variable names:

  • start with lower or upper case letter or _
  • followed by lower or upper case letters or _ or digits
  • names of builtin functions allowed (but not recommended)
  • names of Python statements not allowed.
# those are fine:
a_b_c = 1
a123 = 2
_aXzA = 3

Basic math

+, *, -, / and parenthesis as usual, ** for exponentiation:

print(2 * (3 + 4) - 7)
7

exponentiation is ** not ^:

print(2 ** 10)
1024
print(13 / 4)
3.25

% for modulo (aka division reminder) computation:

print(13 % 4)
1

Integers do not overflow in Python:

x = 2 ** 62  # this can be represented by a 64 bit integer.
y = 2 ** 63  # this overflows in 64 bit integers
print(x, y)
4611686018427387904 9223372036854775808

There is no distinction between single or double precision floats, the Python float type is always with double precision, but may overflow:

print(2.0 ** 1000)
print(2.0 ** 1500)
1.0715086071862673e+301
---------------------------------------------------------------------------
OverflowError                             Traceback (most recent call last)
<ipython-input-13-4dd8e105c5a9> in <module>()
      1 print(2.0 ** 1000)
----> 2 print(2.0 ** 1500)

OverflowError: (34, 'Result too large')

Math with the math module

  • Python ships with extra modules collected in the so called "Python standard library".
  • The documentation on http://python.org is complete but http://pymotw.com/2/ is more detailed and shows more examples.
  • Use import to use them.
import math
print(math)
<module 'math' from '/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/lib-dynload/math.cpython-35m-darwin.so'>

Now functions and constanstants are "attached" to math:

print(math.pi)
3.141592653589793
print(math.sin(1.0))
0.8414709848078965

Python help system:

print(help(math))   # lots of output
print(help(sum))

Alternative ways to import functions, values, etc:

from math import pi, e, sin, log
print(log(e))
1.0

from math import * works, it imports everything (which might be a lot), but is dangerous, for example this overwrites a variable e:

e = 123
from math import * 
print(e)
2.718281828459045

Simple input output input

name = input("what is your name ? ")
print("hi", name, "how do you do ?")
print(type(name))
what is your name ? uwe
hi uwe how do you do ?
<class 'str'>

input always returns a string. To work with numbers we need type conversion.

E.g.:

print(float("1.23"))
1.23
print(int("42"))
42

So if we want the user to enter a floating point number and we want to work with the input as number, we write:

x = float(input("give me a number: "))
print(x, "squared is", x * x)
give me a number: 1.23
1.23 squared is 1.5129

Exercise session 1

  1. Enter print(42) and run this script
  2. What is the reminder of $2^{63}$ divided by $13$ ?
  3. Which number is larger: $\pi^e$ or $e^\pi$ ?
  4. Write a script which asks for the diameter of a circle and computes its area and circumference.

2. String basics

Strings are defined using delimiters " or ' or """ or ''':

If you choose " as delimiter you may use ' in the string and the other way round.

print("hi, it's time to go")
hi, it's time to go
print('this is "a quote"')
this is "a quote"
long = """multi line string ...
it works"""

print(long)
multi line string ...
it works

The repr function gives us more detailed information:

print(repr(long))
'multi line string ...\nit works'

Comments in Python

# this is a single line comment

print(3)

"""
this is a multi line comment
the comment ends here
"""

print(4)
3
4

String "algebra":

print("3.1" + '41')
3.141
print(3 * "\o/ ")
\o/ \o/ \o/ 
print(len("12345"))
5

String methods

Many string operations are "attached" to string object.

Python strings are immutable ("const"). So string methods never change the string object in place. So for example the following upper method creates a new string:

# transforms string "hello" to a new string:
greeting = "hello"
print(greeting.upper())
print(greeting)        # unchanged !
HELLO
hello

Method calls can be chained. For example this startswith method ...

print("hi you".startswith("hi"))
True

... can be called on the result of upper():

print("hi you".upper().startswith("HI"))
True

String "slicing"

Use [..] for accessing parts of a string, counting start with 0.

print("Python"[1])
y

Negative indices start at the end, -1 is the last character, -2 the character before the last character and so on:

print("Python"[-2])
o

substrings using [m:n], the right limit is exclusive:

print("Python"[2:4])
th
print("Python"[1:-1])
ytho

Exercise session 2

  1. Try to forecast the values of the variables in the following snippet using pen and paper, use help(str.rstrip) etc for looking up the used methods. If you are sure that you know what the output will be, use Python to validate your results.
values = "012" * 3 + """'a'bc"""
a = values[0:2] + values[0] + values[2:3] 
b = a + values[len(values) - 2].upper()
c = a.rstrip('2')
d = a.find("A")

3. Python functions

Use

def <name>(<arg_0>,...):
    <body>

to define a Python function:

def times_3(x):
    print("you called me with arguemnt", x)
    return 3 * x
    
times_3(42)
you called me with arguemnt 42
126

NO BRACES: As soon as identation decreases the body of the function is completed.

Good style:

  1. use four spaces (or multiples) for identation
  2. no TABs (PyCharm automatatically replaces TAB by spaces !)

"Duck Typing"

No type declaration for the arguments. During execution of the function Python determines if operations on arguments fit:

print(times_3("ab"))
you called me with arguemnt ab
ababab

Python supports multiple return values:

def sum_and_diff(x, y):
    sum_ = x + y
    diff = x - y
    return sum_, diff

a, b = sum_and_diff(7, 3)
print("sum is", a)
print("diff is", b)
sum is 10
diff is 4

Python doc strings

If you use a string directly after the def ... line we call this string a "doc string" which allows us to contribute to the built in help system:

def average_3(a, b, c):
    """this function computes the average of
    three given numbers
    """
    return (a + b + c) / 3.0

help(average_3)
Help on function average_3 in module __main__:

average_3(a, b, c)
    this function computes the average of
    three given numbers

Exercise Session 3

  1. Repeat the examples above, starting at "Multiple return values"
  2. Write a function which takes the diameter of a circle and returns its area and circumference

4. If / elif / else

Logical values

Python has a type bool which can take two values True and False:

ok = True
print(ok, type(ok))
True <class 'bool'>

Comparisons:

Logical values result from comparing numbers:

notation meaning
a < b a is less than b
a > b a is greater than b
a <= b a is less than or equal to b
a >= b a is greater than or equal to b
a == b a is is equal to b
a != b a is not equal to b

Comment:

  • = (which is variable assignment) is a statement ("it does something")
  • == (which is test for equality) is an expression (it can be evaluated to compute a value)

Logical computations:

Logical values can be combined

notation meaning
a and b True if a and b are True
a or b True if a or b are True
not a True if a is False else False
print(3 > 4 or 4 > 3)
True
print(3 < 7 and 7 < 12)
True

if / elif /else

  • Python uses if, elif and else keywords for branching code execution.
  • No else if !!!
  • And n switch statement.
  • NO BRACES: The level of identation defines the blocks, no "end" statement or braces !
def test_if_even(x):
    if x % 2 == 0:
        print(x, "is even")
    else:
        print(x, "is odd")
        
test_if_even(12)
12 is even

Identations can be nested:

def some_tests(x):
    if x > 0:
        if x % 2 == 0:
            print(x, "is positive and even")
        else:
            print(x, "is positive and odd")
    elif x == 0:
        print(x, "is zero")
    else:
        print(x, "is negative")
some_tests(4)
4 is positive and even
some_tests(-1)
-1 is negative

Exercise block 4

  1. Repeat the examples above
  2. Write a function which takes one value and doubles this if the value is even, else return the value unchanged. So the function returns 4 for input 2 and 3 for input 3.
  3. Write a function which takes a value and tests if it is a multiple of three and if it is a multiple of four. The functions prints an apropriate message for all four cases.

5. Python loops

Python has while, continue and break:

x = 7
while x > 0:
    x = x - 1
    if x % 2 == 0:
        continue   # skips rest of body of while
    print(x)
    if x % 3 == 0:
        break      # quit body of while
        
print("done")
5
3
done

Python lists

Python has some container types for collecting values. list is one such a type:

li = [1, 2, 4, 8]
print(li)
[1, 2, 4, 8]

length of a list:

print(len(li))
4
print(type(li))
<class 'list'>

The empty list is []:

print(type([]))
print(len([]))
<class 'list'>
0

List of strings:

li = ["hi", "ho"]

Mixed types:

li = [1, 2.0, True, "hi"]
print(li)
[1, 2.0, True, 'hi']

More about lists below.

for loops

Loops with for can loop over so called "iterables" for example over lists.

for name in ["urs", "uwe", "guido"]:
    print("I say hi to", name)
I say hi to urs
I say hi to uwe
I say hi to guido
def sumup(li):
    sum_ = 0.0
    for item in li:
        sum_ += item   # same as sum_ = sum_ + item
    return sum_

print(sumup([1, 2, 3]))
6.0

For implementing a "counting loop" use range which returns an iterable:

for i in range(3):
    print(i, "squared is", i * i)
0 squared is 0
1 squared is 1
2 squared is 4

Exercise block 5

  1. Repeat examples above
  2. Write a function which takes a list of numbers and computes their average
  3. Modify this function so that it computes the average of all even numbers in the list.

6. More about Python container types

Lists

Python lists collect data, types may be mixed and the list may be as long as your computers memory allows.

Some usefull, but not all list methods:

li = [1, 2, 3]
li.append(0)
print(li)
[1, 2, 3, 0]
print(len(li))
4
li.sort()
print(li)
[0, 1, 2, 3]
print(min(li), sum(li), max(li))
0 6 3

List slicing, similar to strings:

print(li[1:-1])
[1, 2]

In contrast to Python strings, which are immutable ("const"), you can use slicing for deletion and replacement of parts of a list as follows:

del li[1:3]
print(li)
[0, 3]
li[1:3] = [777, 888, 999]
print(li)
[0, 777, 888, 999]

Tuples

"immutable" lists. Use paranthesis instead of brackets:

  • mixed types allowed too
  • slicing works
a = (1, 3, 5)
print(a)
print(type(a))
(1, 3, 5)
<class 'tuple'>

Rules of thumb:

  • Use lists for collecting data of same type
  • Use tuples for grouping data of differnt type
tp = (1, 2, (1, 2), "")
print(tp[1:-1])
(2, (1, 2))

Empty tuple is (), for one element tuples use (x,) notation:

print((1,))  # prints tuple with one element
print((1))   # prints integer number 1
(1,)
1

Dictionaries

Dictionaries, aka "hash tables" or "look up tables" allow presentation of two column tables.

For example:

surname name
monty python
curt cobain
surnames = { 'monty': 'python',
             'curt' : 'cobain',
           }
print(surnames)
{'curt': 'cobain', 'monty': 'python'}

You see above that printing the dictionary has a different order than in its definition. Dictionaries are only for representing a mapping from values in the left column of the table to their counterpart in the right column. Ordering is not respected.

To lookup up a value use brackets:

print(surnames["monty"])
python

You can put values into a dictionary like this:

surnames["uwe"] = "schmitt"
print(surnames)
{'uwe': 'schmitt', 'curt': 'cobain', 'monty': 'python'}

Size of a dictionary:

print(len(surnames))
3

Left column of table are "keys":

print(surnames.keys())
dict_keys(['uwe', 'curt', 'monty'])

Right column are "values":

print(surnames.values())
dict_values(['schmitt', 'cobain', 'python'])

Both are iterables again:

for name in surnames.keys():
    print(name)
uwe
curt
monty

The empty dictionary is {}:

d = {}
print(d)
print(len(d))
{}
0

Restrictions

  • values of dictionaries may have arbitrary type
  • keys must be immutable (so: on lists, but tuples, int, float, str, bool, ...)

Lookup of non existing keys:

print(surnames["jesus"])
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-84-d957f9edddbc> in <module>()
----> 1 print(surnames["jesus"])

KeyError: 'jesus'
print(surnames.get("monty"))
print(surnames.get("jesus"))
python
None

Dictionaries may have different types for keys and values:

dd = { 3: 9, 4: "four", 5: {25: 125}, 6: (1,2), 7:range(10), 8: None}
print(dd)
{3: 9, 4: 'four', 5: {25: 125}, 6: (1, 2), 7: range(0, 10), 8: None}
print(dd[5][25])
125

Why dictionaries ?

  • very fast "in memory database" (lookup and many other operations are $O(1)$)
  • short, readable and concise syntax

Exercises block 6

  • repeat the examples above

7. File I/O

Open a file (the traditional way)

open(path, mode) returns an file object which you can use for reading and writing. mode maybe "r", "w", "a" (or some other types...) for "reading", "writing" and "appending":

fh = open("say_hi.txt", "w")
fh.write("hi")

# always close a file because data may be in buffer:
fh.close()

This is dangerous if you forget to close the file or if an error resumes programm execution before the close method is called !

Background: most OS do not write immediatly to disk if you call write but collect data until an internal memory region (buffer) is filled. So you never now exactly what is still in the buffer and what is on disk. Only after closing or calling fp.flush() you can be sure that your data is on disk.

Open a file (modern way) using with:

Since Python 2.5 we have the with statement. This statement executes the following body in a secure way, so that the file is always closed, even in case of an error inside the body.

with open("hiho.txt", "w") as fh:
    fh.write("hi")
    fh.write("ho")
#file content of hiho.txt
hiho

Using print(to write to a file

with open("say_hi.txt", "w") as fh:
    print("say", file=fh)
    print("hi", file=fh)

The line below is not Python code and is only supported by the software I used for this tutorial. It prints the content of the file named "say_hi" , so you see the result from the snippet above:

#file content of say_hi.txt
say
hi

Reading a file

The read method returns the full content in one string, we open the file in read mode "r" here:

with open("say_hi.txt", "r") as fh:
    print(repr(fh.read()))
'say\nhi\n'

read without arguments reads the full file. So if you call read on a file containg 1 MB of data you get a very long string.

But for text files there are two more comfortable ways to read: readlines returns the file line by line in a list of strings:

with open("say_hi.txt", "r") as fh:
    print(fh.readlines())
['say\n', 'hi\n']

Comment: there is also a method called readline (no s at the end !) which only reads one line. So take care the use the right method name.

What is nice in Python is that you can loop over the lines in a file using for:

with open("say_hi.txt", "r") as fh:
    for line in fh:
        print(repr(line))
'say\n'
'hi\n'

Performance tip: use the for loop for iterating over a file. For huge files this only reads as much bytes as needed in every iteration and thus works for files which are larger than your computers memory !

Reading and writing csv files

This is not covered in this course, but look at https://pymotw.com/3/csv/index.html !

Serialisation

Serialisation writes (even nested) data structures in a binary format to a disk. You can recover this data later on easily. In Python serialisation is called "pickling" like conserving vegatables in a jar.

# this is a complex data structure:
data = (1, { 1: 2, 3: [1, 2, 3], "s": (1, 2)})
print(data)
(1, {1: 2, 3: [1, 2, 3], 's': (1, 2)})
import pickle
with open("data.bin", "wb") as fh:  # open in "write binary" mode.
   pickle.dump(data, fh)
# this is how it looks like on disk:
#file content of data.bin
�K}q(KKK]q(KKKeXsqKK�qu�q.
# now recover your complex data structure:
with open("data.bin", "rb") as fp:
    recovered = pickle.load(fp)
print(recovered)
print(recovered == data)
(1, {1: 2, 3: [1, 2, 3], 's': (1, 2)})
True

Exercise block 7

  1. Repeat the examples starting with "Open a file (modern way) using with".
  2. Write a script which writes square numbers 1, 4, 9, ..., 100 line by line to a text file, check the content with your file system explorer then write some code to read the numbers again.

Optional exercises

  1. Write a 10 times 10 multiplication table to a text file.
  2. Create a text file with some lines of text. Then implement a function which reads the words from the text and counts how often the single words occur in the file ("word histogram"). Hint: strings have a split method, use a dictionary for counting

8. Classes

This section assumes that you already had an introduction to object oriented programming in some programming language and will show how classes work in Python.

A class in Python is declared with the class statement, methods are defined with def like functions.

Remember: a class is a "recipe" or "template" for the creation of objects. Or the other way round: a object is an instance of a class.

import math

class Vector2D:
    
    def __init__(self, x0, y0):
        self.x = x0     # set attribute x
        self.y = y0     # set attribute y
        
    def length(self):
        """method which computes length of Vector2D"""
        return math.hypot(self.x, self.y)
    
# create an instance, this creates the object and
# calls __init__ with args 1 and -1:
p = Vector2D(1, -1)

# now we can access attributes
print(p.x, p.y)

# and call method
print(p.length())
1 -1
1.4142135623730951
  • class Vector2D: provides the name of your class and the following indented code block decares the methods of the class.

  • __init__ it the initializer method which is called if you instantiate Vector2D(1, -1). It sets the attributes x and y.

  • There is no type declaration for attributes, you just set them inside __init__.

  • There are no rules for "private" or "protected" attributes and methods. A user of a class can access every attribute and method.

  • But: it is common practice to use names starting with a single _ for private attributes and methods.

  • self is the current instance of the object. (In C++ / Java we have this instead).

  • In Python you have to use self as the first parameter when you declare methods, but you do not provide it if you call the method.

Some internals

Methods are attached to the class, so for examle you can access

print(Vector2D.__init__)
print(Vector2D.length)
<function Vector2D.__init__ at 0x1061cca60>
<function Vector2D.length at 0x1061ccd08>

If you call p.length() Python looks up the class of p which is Vector2D and translates this to Vector2D.length(p), so self is set to p if you call Vector2D.length and so the computation works on attributes of p.

Special methods

There are many other special methods having names starting and ending with __ like __init__. For example __str__ is called whenever you convert a object to a string representation. This is the case when you print(the object:

import math

class Vector2D:
    
    def __init__(self, x0, y0):
        self.x = x0
        self.y = y0
        
    def length(self):
        return math.hypot(self.x, self.y)
    
    def __str__(self):
        return "Vector2D(x=%s, y=%s, lenght=%s)" % (self.x, self.y, self.length()) # attribute and method access here !
    
p = Vector2D(1, -1)

# this calls the __str__ method:
print(p)
Vector2D(x=1, y=-1, lenght=1.4142135623730951)

Or you can implement __add__ which is called if you write p1 + p2 for two instances of Point:

import math

class Vector2D:
    
    def __init__(self, x0, y0):
        self.x = x0
        self.y = y0
        
    def length(self):
        return math.hypot(self.x, self.y)
    
    def __str__(self):
        return "Vector2D(x=%s, y=%s, length=%s)" % (self.x, self.y, self.length()) # attribute and method access here !
    
    def __add__(self, other):
        """ is called when you execute self + other """
        return Vector2D(self.x + other.x, self.y + other.y)
    
p = Vector2D(1, -1)
q = Vector2D(1, 1)

# now call p.__add__(q) which is 
# the same as Vector2D.__add__(p, q):
print(p + q)
Vector2D(x=2, y=0, length=2.0)

There are many other special functions for customizing your objects, see https://docs.python.org/2/reference/datamodel.html#special-method-names

Inheritance

Say we want to create a class which inherits Vector2D by giving it an extra attribute name. This can be implemented as follows:

class NamedVector2D(Vector2D):
    
    def __init__(self, name, x0, y0):
        super().__init__(x0, y0)   # call __init__ in base class
        self.name = name
        
    def __str__(self):
        """ overrides __str__ from Vector2D """
        return "NamedVector2D(name=%s, x=%s, y=%s)" % (self.name, self.x, self.y)
    

# create instance
v1 = NamedVector2D("v1", 1.0, 2.0)

# access attributes
print(v1.x, v1.y, v1.name)

# calls __str__:
print(v1)

# calls method in base class !
print(v1.length())

# this is in base class too, see result:
print(v1 + v1)
1.0 2.0 v1
NamedVector2D(name=v1, x=1.0, y=2.0)
2.23606797749979
Vector2D(x=2.0, y=4.0, length=4.47213595499958)

Exercise block 8

  1. Repeat the examples above.
  2. Extend Vector2D to implement a method scale which takes a float x and scales the attributes x and y internally.
  3. Implement a method __mul__ which takes another vector and returns the dot product (scalar product) of both. __mul__ is called if you use v1 * v2.
  4. Create a class ComplexNumber which inherits Vector2D and reimplements __mul__ for complex arithmethic. Reimplement __str__ to achieve output in the style of 1.0 + 2i.

9. tuple unpacking, enumerate, list comprehensions

Tuple unpacking allows taking values from a tuple without index access, so you need less code and it is often more readable. Instead of writing

tp = (1, 2, 3)
a = tp[0]
b = tp[1]
c = tp[2]
print(a + b + c)
6

... you can write:

a, b, c = tp
print(a + b + c)
6

You can ommit parantheses for declaring a tuple:

a, b, c = 1, 2, 3
print(a + b + c)
6

So: multiple return values of function is nothing else than returning tuples followed by tuple unpacking.

Good style: use __ for not needed values if you do tuple unpacking. So if you only want to unpack a use the following style to avoid declaration of variables you will not use later on:

tp = (1, 2, 3)
a, __, __ = tp

Tuple unpacking is handy for exchanging values, you need no temporary variables:

print(a, b)
a, b = b, a
print(a, b)
1 2
2 1

If this was to fast, here is a more detailed implementation:

print(a, b)
tp = (b, a)  # creates tuple
a, b = tp    # unpacks the values and overwrites variables "a" and "b" with new values !
print(a, b)
2 1
1 2

Before we demonstrate tuple unpacking in for statements, we introduce the zip function which takes 2 or more lists and "zips" the elements to a list of tuples similar to a zipper on your jacket:

a = [11, 22, 33]
b = ["a", "b", "c"]

print(zip(a, b))
print(list(zip(a, b)))
<zip object at 0x1061aef88>
[(11, 'a'), (22, 'b'), (33, 'c')]

So zip iterates over tuples. Tuple unpacking with for now does tuple unpacking for every tuple in the given list:

for num, char in zip(a, b):
    print(num, char)
11 a
22 b
33 c

If you want to iterate over a list and you want to count at the same time enumerate is handy:

for i, char in enumerate(b):
    print(i, char)
0 a
1 b
2 c

List comprehensions

List comprehensions allow creation and transformation of lists in a comprehensive and readable way. For example the following two lines ...

squares = [i * i for i in range(6)]
print(squares)
[0, 1, 4, 9, 16, 25]

... are equivalent to

squares = []
for i in range(6):
    squares.append(i * i)
print(squares)
[0, 1, 4, 9, 16, 25]

But you can filter too:

squares_of_odds = [i * i for i in range(6) if i % 2 == 1]
print(squares_of_odds)
[1, 9, 25]
squares_of_odds = []
for i in range(6):
    if i % 2 == 1:
        squares_of_odds.append(i * i)
print(squares_of_odds)
[1, 9, 25]

We used range only for demonstration, you take any other iterable instead:

words = ["hi", "this", "is", "list", "comprehension"]
print([w.upper() for w in words if len(w) % 2 == 0])
['HI', 'THIS', 'IS', 'LIST']

10. Exception handling: try / except / finally

Python throws exceptions in case of errors:

x = 1 / 0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-119-f9f847a0a080> in <module>()
----> 1 x = 1 / 0

ZeroDivisionError: division by zero

But you can "catch" exceptions:

try:
    x = 1 / 0
except ZeroDivisionError:
    print("oops")
oops

You can raise your own exceptions:

import math
def fun(number):
    if number < 0.0:
        raise Exception("%s is negative! " % number)
    return math.sqrt(number)
print(fun(2.0))
1.4142135623730951

If you provoke an exception, you see a so called stack trace which indicates how this error was triggered:

# read the output below line by line !!!
print(fun(-1.0))
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-123-c513a8d426e1> in <module>()
      1 # read the output below line by line !!!
----> 2 print(fun(-1.0))

<ipython-input-121-0e2eca09054d> in fun(number)
      2 def fun(number):
      3     if number < 0.0:
----> 4         raise Exception("%s is negative! " % number)
      5     return math.sqrt(number)

Exception: -1.0 is negative! 

Exceptions can be handled on different levels:

def test(x):
    return fun(x - 1)

for i in range(3):
    try:
        print(test(i))
    except:
        print("test failed for argument", i)
test failed for argument 0
0.0
1.0

11. More about functions

Functions may have default values:

def call(a, b=3, c=4):
    print("a =", a, ", b =", b, "and c =", c)
call(1, 2, 3)
a = 1 , b = 2 and c = 3
call(1, 2)
a = 1 , b = 2 and c = 4
call(2)
a = 2 , b = 3 and c = 4

Why ? Good for sensible default values (e.g. tolerances or iteration counts in numerical algorithms)

You can name the arguments when you call a function:

call(1, c=5)
a = 1 , b = 3 and c = 5
call(c=5, a=3, b=4)
a = 3 , b = 4 and c = 5

Why ? Meaning of parameters becomes obvious if you read the function call, no need to look up the function definition.