Python Introduction#

There are three main ways you might want to run python code in:

  1. Executing a python file

    Just like running a bash script or any other program: simply save your python code in a file with “.py” extension and run it using the python interpreter

    python filename.py
    

    This the most performant way to run scripts, but the less flexible.

  2. Interactive python shell

    If you run the ipython interpreter, you can type individual lines of python code and interactively see the return value.

    This is useful to quickly check the return type of a function or the documentation of an object, but it is not suited to write large program in.

  3. Jupyter Notebook

    A mixture of the previous two. By starting the jupyter-notebook command (after installing it) you can write a file with several sections which can be interactively executed. It is ideal for quick prototyping and testing.

Python installations typically come with a package manager called pip. Here are some examples on how to install libraries with pip:

python -m pip install ipython
python -m pip install pyserial
python -m pip install python-can

Let’s write some python code#

Basic I/O#

Hello world in python:

print("Hello, World!")
Hello, World!

print is a global function, so it can be called from any point of the code and it will print some string on the standard output.

Now something more complex:

#!/usr/bin/env python3

# Request the user name from standard input
user_name = input("Enter your name: ")
print("Hello,", user_name)
Hide code cell output
---------------------------------------------------------------------------
StdinNotImplementedError                  Traceback (most recent call last)
Cell In[2], line 4
      1 #!/usr/bin/env python3
      2 
      3 # Request the user name from standard input
----> 4 user_name = input("Enter your name: ")
      5 print("Hello,", user_name)

File /usr/local/lib/python3.12/site-packages/ipykernel/kernelbase.py:1281, in Kernel.raw_input(self, prompt)
   1279 if not self._allow_stdin:
   1280     msg = "raw_input was called, but this frontend does not support input requests."
-> 1281     raise StdinNotImplementedError(msg)
   1282 return self._input_request(
   1283     str(prompt),
   1284     self._parent_ident["shell"],
   1285     self.get_parent("shell"),
   1286     password=False,
   1287 )

StdinNotImplementedError: raw_input was called, but this frontend does not support input requests.

First, any line stat starts with a # is a comment, and is not executed by the python interpreter.

The first line is an Unix “Shebang”, it’s useful on non-windows operating systems to tell the shell which interpreter to use when the file is executed as a program (if the executable flag is enabled).

In the line user_name = input(), we are initializing the variable user_name with a string read from the terminal. In python, we don’t need to declare variables before using them. Also, since we are not inside a function or a class, the variable user_name is global.

Finally, we output a greeting to the user, showing that the function print accepts multiple arguments.

Important: even though we don’t need to declare variables and their types like in C++/Java, variables still have a type. We can check the type of a variable with the type function:

print(type(3))
<class 'int'>
print(type('Hello'))
<class 'str'>
print(type(3.14))
<class 'float'>

Math#

Next, let’s see some mathematical operations. Python syntax for math is similar to C.

a = 3 + 6
print("a =", a)
a = 9
a -= 5
print("a =", a)
a = 4
a++  # Invalid python code!
  Cell In[8], line 1
    a++  # Invalid python code!
         ^
SyntaxError: invalid syntax
print(3 / 2)
1.5
print(4 / 2)
print(type(4 / 2))  # Careful: a division always returns a float
2.0
<class 'float'>
print(3 // 2)  # To cast the output of a division to the int of the floor, simply use //
print(type(3 // 2))
1
<class 'int'>

We can use all the typical bitwise operations from C. The hex function converts a number into its hexadecimal string representation

a = 0x12345678
a = (a & 0xffff0000) >> 16 | (a & 0x0000ffff) << 16
a = (a & 0xff00ff00) >> 8 | (a & 0x00ff00ff) << 8
a = (a & 0xf0f0f0f0) >> 4 | (a & 0x0f0f0f0f) << 4
a = (a & 0xcccccccc) >> 2 | (a & 0x33333333) << 2
a = (a & 0xaaaaaaaa) >> 1 | (a & 0x55555555) << 1
print(hex(a))
# What did this code do?
0x1e6a2c48

However, always remember that integers in python can be infinitely long (as long as there is memory on your system!)

a = 0xcafebabe << 128
print(hex(a))
0xcafebabe00000000000000000000000000000000

So if you want to simulate uint32_t from C, simply use & 0xffffffff

print(hex(0xdeadbeef << 8 & 0xffffffff))
print(hex(-1 & 0xffffffff))
0xadbeef00
0xffffffff

There are also some useful global math functions

print(abs(-123))
print(max(1, 2, 3))
print(min(1, 2, 3))
123
3
1

But for more math functions, import the math module

import math
print(math.ceil(1.2))
print(math.pi)
print(math.log(100, 10))
print(math.log(1000, 10))  # This highlights that decimal numbers are 64-bit double precision floats
2
3.141592653589793
2.0
2.9999999999999996

Lists#

python includes has some useful data structures.

Lists can be thought of as arrays from C, but they can contains objects of different types and can be extended and shrunk after their creation

a = [1, 2, "Hello", "World"]
print(a)
[1, 2, 'Hello', 'World']
print(type(a))
<class 'list'>
print(len(a))  # Query the length of the list
4
print(a[2])  # Check what is in the third position
Hello
print(a.pop())  # removes element from the end of the list, like a stack
World
print(a)
[1, 2, 'Hello']
a.append(4)  # appends element to the end of the list
print(a)
[1, 2, 'Hello', 4]
print(a.pop(2))  # Remove element from specified location
Hello
print(a)
[1, 2, 4]
a.insert(2, 'Three')  # Insert element at specified location
print(a)
[1, 2, 'Three', 4]
a[2] = 3  # Replace element at specified location
print(a)
[1, 2, 3, 4]
a = [9, 1, 5, 3, 2, 5, 1, 6]
print(a[2:5])  # we can extract a subsed of a list like this
[5, 3, 2]
print(a[-1])  # Or just the last element
6
print(a[1::2])  # Every second element, starting from the second
[1, 3, 5, 6]
print(a[::-1])  # Invert the order
[6, 1, 5, 2, 3, 5, 1, 9]
print(1 in a)  # Ask if the list contains an element of value 1
True

For a complete list of the methods included in list objects, simply execute help(list) in ipython.

Tuples#

Tuples are similar to lists, but they can not be modified after their creation

t = (1, 'Two', math.pi)
print(t)
print(type(t))
(1, 'Two', 3.141592653589793)
<class 'tuple'>
print(t[1])
Two
# All these statements will not work
t.pop()
t[0] = 1
t.append(4)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[35], line 2
      1 # All these statements will not work
----> 2 t.pop()
      3 t[0] = 1
      4 t.append(4)

AttributeError: 'tuple' object has no attribute 'pop'

However, we can create a list out of a tuple, modify it, then create a new tuple from the modified list:

a = list(t)
a.append(4)
t = tuple(a)
print(t)
(1, 'Two', 3.141592653589793, 4)

Tuples can be used to assign multiple variables in one statement:

t = (1, 'pasta', 12.1)
a, b, c = t
print("a is", a)
print("b is", b)
print("c is", c)
a is 1
b is pasta
c is 12.1

Dictionaries#

Next we have dictionaries. Dictionaries are like hash maps in other languages:

d = {}  # Initialize an empty dictionary
print(d)
print(type(d))
{}
<class 'dict'>
# The keys and values of a dictionary can be any type
d['name'] = 'Enrico'
d['age'] = 31
d[5] = 3.14
print(d)
{'name': 'Enrico', 'age': 31, 5: 3.14}
del d[5]  # Remove the element with key 5 from the dictionary 
print(d)
{'name': 'Enrico', 'age': 31}
print(list(d.keys()))  # We can get the list of the keys of a dictionary
['name', 'age']
print(list(d.values()))
['Enrico', 31]
print(list(d.items()))
[('name', 'Enrico'), ('age', 31)]

Sets#

Finally, we have sets. Sets are similar to list, but they don’t allow duplicates

greetings = {'Hello', 'Hi'}
print(greetings)
print(type(greetings))
{'Hi', 'Hello'}
<class 'set'>
greetings.add('Good Morning')
print(greetings)
{'Hi', 'Hello', 'Good Morning'}
greetings.add('Hi')
print(greetings)
{'Hi', 'Hello', 'Good Morning'}

strings#

Strings are immutable arrays of characters which are already encoded:

s = 'Hello, World!'
print(s)
print(type(s))
Hello, World!
<class 'str'>
s[1] = a  # Replacing a single character does not work
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/home/jonas/Documents/knowledgebase/dissecto_kb/chapters/python_intro.ipynb Cell 72 line 1
----> <a href='vscode-notebook-cell:/home/jonas/Documents/knowledgebase/dissecto_kb/chapters/python_intro.ipynb#Y243sZmlsZQ%3D%3D?line=0'>1</a> s[1] = a  # This 

TypeError: 'str' object does not support item assignment
s = "Viel Spaß!"
print(s)
Viel Spaß!
s = "😁😛😋🤣"
print(s)
😁😛😋🤣
print(len(s))
4
print(s[2])
😋
# Each character is a string object itself!
print(type(s[2]))
<class 'str'>
s = "Hello, World!"
print(ord(s[1]))  # ord converts character to ASCII code (or Unicode)
101
print(chr(101))  # chr does the inverse
e
a = "Hello, "
b = "World!"
print(a + b)  # String concatenation
Hello, World!
print("+-" * 20)  # String repetition
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
s = "Hello, World!"
print(s[::-1])  # String inversion
!dlroW ,olleH
# Sprintf-like string formatting using %
s = "%d + %d = 0x%x" % (100, 60, 160)
print(s)
100 + 60 = 0xa0

bytes#

Perhaps one of the most confusing elements of python 3 for novices are byte string (bytes).

They function like strings but they can only contain bytes from 0 to 255, no unicode characters!

bytes are typically used for buffers for binary communication and binary files.

We can declare bytes strings in several different ways:

a = b"Hello, World!"
print(a)
b = bytes([6, 32, 54, 13])
print(b)
c = bytes.fromhex("deadbeef")
print(c)
b'Hello, World!'
b'\x06 6\r'
b'\xde\xad\xbe\xef'

By default, when printing a bytes string, python will try to display it as an escaped C string.

Sometimes this is not convenient, so we can convert it to an hexadecimal string representation using the hex() method:

b = bytes([6, 32, 54, 13])
print(b)
print(b.hex())
b'\x06 6\r'
0620360d

We can not initialize a byte string with an unescaped non-ascii character

b0 = b"Nils Weiß"
  Cell In[61], line 1
    b0 = b"Nils Weiß"
                     ^
SyntaxError: bytes can only contain ASCII literal characters

But we can encode strings to bytes and decode bytes to strings:

s = "Nils Weiß"
b0 = s.encode('latin1')
print(type(b0))
print(b0)
<class 'bytes'>
b'Nils Wei\xdf'
b1 = s.encode('utf-8')
print(b1)
b'Nils Wei\xc3\x9f'
print(b1.decode('utf-8'))
Nils Weiß

Flow control#

Python has if-elif-else statements, but no switch statements.

Instead of a switch, we can create many elif branches:

user_string = input("Give me a number: ")
number = float(user_string)
if number % 2 == 1:
    print("The number was odd")
elif number % 2 == 0:
    print("The number was even")
else:
    print("The number was neither odd nor even")
Give me a number: 4
The number was even

We can combine boolean conditions with and, or and not:

year = 2000
if (year % 4 == 0) and not (year % 100 == 0) or (year % 400 == 0):
    print("%d is a leap year" % year)
2000 is a leap year

We have two loop types: for and while. No do..while.

While loops are easy and work as they work in C, Java and others:

elements = []
stop = False
while not stop:
    user_string = input("Enter a number or an empty string to stop")
    if user_string == "":
        stop = True
    else:
        elements.append(user_string)

print(elements)
Enter a number or an empty string to stop
[]

In python, a for loop iterates over iterable objects (such as lists, strings…)

for a in [1, 12, 'Blue']:
    print(a)
1
12
Blue

The global function range generates an iterable range of numbers:

for i in range(5):
    print(i)
0
1
2
3
4

Exceptions are also possible, and they work similarly to C++/Java:

try:
    user_string = input("Give a number: ")
    number = int(user_string)
except Exception as ex:
    print("The following Exception happened ", ex)
else:  # Optional: only executed if no exception happened
    print("No exception happened, all good!")
finally:  # Optional: always executed, used for cleanup
    print("Processing finished, doing some cleanup")
Give a number: 5234
No exception happened, all good!
Processing finished, doing some cleanup

Some objects that require cleanup can be created in a with statement, to guarantee they get disposed correctly:

with open("file.txt", "w") as fd:
    fd.write("Hello, World!")
# fd is guaranteed to be flushed and disposed correctly after the with block

Functions#

Functions are declared as follows:

def add_numbers(a, b):
    answer = a + b
    return answer

print(add_numbers(1, 3))
4

You can have optional arguments by assigning the default value in the argument declaration:

def greet(username='User'):
    print("Hello,", username)

greet()
greet("Enrico")
Hello, User
Hello, Enrico

Functions are first class citizens.

For example, the list.sort method takes a function as the optional argument key

def invert_number(n):
    return -n

a = [5, 4, 1, 2, 3]
a.sort()
print("Sorted list:", a)

a.sort(key=invert_number)
print("List sorted from largest to smallest:", a)
Sorted list: [1, 2, 3, 4, 5]
List sorted from largest to smallest: [5, 4, 3, 2, 1]

Anonymous functions can be generated inline, but only if they are composed of a single statement:

a = [4, 5, 1, 2, 3]
a.sort(key=lambda n: -n)
print("List sorted from largest to smallest:", a)
List sorted from largest to smallest: [5, 4, 3, 2, 1]

Functions can be nested and they automatically capture the variables from the outer scope:

def make_greeter(name):
    def greeter():
        print("Hello,", name)
    
    # Return a "function pointer" that includes the captured name
    return greeter

greeter1 = make_greeter("Enrico")
greeter2 = make_greeter("Nils")

greeter1()
greeter2()
Hello, Enrico
Hello, Nils

Classes#

class Package:
    def __init__(self, weight, width, height, depth):
        # This is the constructor
        # We declare all the local variables here
        self.weight = weight
        self.size = (width, height, depth)
        self.destination_city = None  # None is like null in Java/C++
        self.destination_address = None

    def volume(self):
        # Instance methods need to have "self" as a first argument
        width, height, depth = self.size
        return width * height * depth

    def density(self):
        # Note that all the object fields and methods are accessed from `self`
        return self.weight / self.volume()
    
    def send_to(self, city, address):
        self.destination_city = city
        self.destination_address = address

# Instantiate the class: like C++/Java, but without the `new` keyword
p = Package(520, 5, 4, 12)
p.send_to("Regensburg", "Franz-Mayer-Strasse 1")

print("Volume of the package is", p.volume())
print("Density of the package is", p.density())
print("Destination city is", p.destination_city)
Volume of the package is 240
Density of the package is 2.1666666666666665
Destination city is Regensburg
class InternatioinalPackage(Package):  # Inheritance
    def __init__(self, weight, width, height, depth):
        # Call the parent constructor statically, we need to provide "self"
        Package.__init__(self, weight, width, height, depth)
        self.destination_contry = None

    def send_internationally(self, country, city, address):
        self.send_to(city, address)
        self.destination_country = country
        
p = InternatioinalPackage(520, 5, 4, 12)
p.send_internationally("Italy", "Padova", "Via G. Gradenigo 6/b")

print("Volume of the package is", p.volume())
print("Density of the package is", p.density())
print("Destination country is", p.destination_country)
Volume of the package is 240
Density of the package is 2.1666666666666665
Destination country is Italy

Outside of __init__, there are several other special method names to override specific behaviors.

Here are some examples:

  • def __del__(self) is the destructor, called when the object is deallocated by the GC

  • def __add__(self, other) overrides addition

  • def __getitem__(self, key) overrides the object[key] indexing operator

  • __enter__ and __exit__override the behavior of a entering and leaving a with block

Advanced python syntax#

Ternary operator#

Python has a ternary operator:

value = value_if_true if condition else value_if_false

It is equivalent to the following C code:

value = condition ? value_if_true : value_if_false
a = 1
print(True if a else False)
True

List comprehension#

We can create a new list applying one operation to every element of an existing list:

a = [3, 2, 6, 9, 2]
doubles = [ n * 2 for n in a ]
print(doubles)
[6, 4, 12, 18, 4]

We can also place a condition on which elements should be included in the list:

a = [3, 2, 6, 9, 2]
odds = [ n for n in a if n % 2 == 1 ]
print(odds)
[3, 9]

Notice the similarities to SQL queries

SELECT users.name
FROM users
WHERE users.age >= 18
result = [
    user.name
    for user in users
    if user.age >= 18
]