You want to learn Python 3 ? Here is what you can do
Tested Configuration:
MacOS: Sierra 10.12
Python: 3
Jupyter notebook: 5.4.0

Note

The guide we will follow is available as a Jupyter Notebook
It is highly recommended to swith to the Jupyter Notebook, since it enables interactions with Python and therefore will show your the Result of the code.

Python objects & Python variables

Why using variables: So that we can refer to things by names that make sense.

Variables names: Only letters, _, or numbers (no spaces or other characters). Must start with a letter or underscore


Objects types

Some of the basic python object types include:


Basic operators

Some of the basic Python operators include:

Operators with higher precedence are evaluated first, and operators with the same precedence are evaluated from left to right.

See https://docs.python.org/3/reference/expressions.html#operator-precedence

# Assigning some numbers to different variables
num1 = 10
num2 = -3
num3 = 7.4
num4 = -.6
num5 = 3
num6 = 7
num7 = 70.70
# Addition
num1 + num2
7
# Subtraction then multiplication
( num3 - num4 ) * num5

24.0
# Exponent
num5 ** num6
2187
# Increment existing variable
num7 += 4
num7
74.7
# Decrement existing variable
num6 -= 2
num6
5
# Multiply & re-assign
num3 *= 5
num3
37.0
# Assign the value of an expression to a variable
num8 = num1 + num2 * num3
num8
-101.0
# Are these two expressions equal to each other?
num1 + num2 == num5
False
# Are these two expressions not equal to each other?
num3 != num4
True
# Is the first expression less than the second expression?
num5 < num6
True
# Is this expression True?
5 > 3 > 1
True
# Is this expression True?
5 > 3 < 4 == 3 + 1
True
# Assign some strings to different variables
simple_string1 = 'an example'
simple_string2 = "oranges "
# Addition
simple_string1 + ' of using the + operator'
'an example of using the + operator'
# Notice that the string was not modified
simple_string1
'an example'
# Multiplication
simple_string2 * 4
'oranges oranges oranges oranges '
# This string wasn't modified either
simple_string2
'oranges '
# Are these two expressions equal to each other?
simple_string1 == simple_string2
False
# Are these two expressions equal to each other?
simple_string1 == 'an example'
True
# Add and re-assign
simple_string1 += ' that re-assigned the original string'
simple_string1
'an example that re-assigned the original string'
# Multiply and re-assign
simple_string2 *= 3
simple_string2
'oranges oranges oranges '
# Note: Subtraction, division, and decrement operators
# do not apply to strings.
list_45_56 = [45,56]
num46 = 454
num46 in list_45_56
False
num46 = 45
num46 in list_45_56
True

Basic containers

Note: mutable objects can be modified after creation. immutable cannot
Note: Containers are objects used to group other objects

The basic container types include:

Strings, lists, and tuples are all sequence types that can use the +, *, +=, and *= operators.

# Assign some containers to different variables
list1 = [3, 5, 6, 3, 'dog', 'cat', False]
tuple1 = (3, 5, 6, 3, 'dog', 'cat', False)
set1 = {3, 5, 6, 3, 'dog', 'cat', False}
dict1 = {'name': 'Jane', 'age': 23, 'fav_foods': ['pizza', 'fruit', 'fish']}
# Items in the list object are stored in the order they were added
list1
[3, 5, 6, 3, 'dog', 'cat', False]
# Items in the tuple object are stored in the order they were added
tuple1
(3, 5, 6, 3, 'dog', 'cat', False)
# Items in the dict object are not stored in the order they were added
dict1
{'age': 23, 'fav_foods': ['pizza', 'fruit', 'fish'], 'name': 'Jane'}
# Items in the set object are not stored in the order they were added
# Also, notice that the value 3 only appears once in this set object
set1
{3, 5, 6, False, 'cat', 'dog'}
# Items in the dict object are not stored in the order they were added
dict1
{'age': 23, 'fav_foods': ['pizza', 'fruit', 'fish'], 'name': 'Jane'}
# Add and re-assign
list1 += [5, 'grapes']
list1
[3, 5, 6, 3, 'dog', 'cat', False, 5, 'grapes']
# Add and re-assign
tuple1 += (5, 'grapes')
tuple1
(3, 5, 6, 3, 'dog', 'cat', False, 5, 'grapes')
# Multiply
[1, 2, 3, 4] * 2
[1, 2, 3, 4, 1, 2, 3, 4]
# Multiply
(1, 2, 3, 4) * 3
(1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4)
num10 = 10
# Add and re-assign
tuple1 += (num10, 'test')
tuple1
(3, 5, 6, 3, 'dog', 'cat', False, 5, 'grapes', 10, 'test')
#re assign num10
num10 = 189
# see that tupl didn't change: it is immutable
tuple1
(3, 5, 6, 3, 'dog', 'cat', False, 5, 'grapes', 10, 'test')

Access data: containers[subscript notation]

# Access the first item in a sequence
list1[0]
3
# Access the last item in a sequence
tuple1[-1]
'test'
# Access a range of items in a sequence
simple_string1[3:8]
'examp'
# evrything until le last 3rd element (not including it)
tuple1[:-3]
(3, 5, 6, 3, 'dog', 'cat', False, 5)
# everything from the 4th element (including it)
list1[4:]
['dog', 'cat', False, 5, 'grapes']

Note: the start is always included, and the end always excluded. This makes sure that s[:i] + s[i:] is always full s

# Access an item in a dictionary
dict1['name']
'Jane'
# Access an element of a sequence in a dictionary
dict1['fav_foods'][2]
'fish'

Python functions

A function is a Python object that performs an action or / and return another object.
You can pass arguments to it: these arguments are treated like variables.

# This is how to define a function
def a_fct(arg1):
    print(arg1)

#and this shorthand
def the_same_fct(arg1):
    print(arg1)
a_fct('this is arg1')
the_same_fct('this is arg1')
this is arg1
this is arg1

Python built-in functions

Here is a small sample of them:

Complete list of built-in functions: https://docs.python.org/3/library/functions.html

# Determine the type of an object
type(simple_string1)
str
# Determine how many items are in a container
print( len(dict1) )
print( len(simple_string2) )
3
24
# Use the callable() function to determine if an object is callable
print(' callable(len)   is ', callable(len) )

# Use the callable() function to determine if an object is callable
print(' callable(dict1) is ', callable(dict1) )
 callable(len)   is  True
 callable(dict1) is  False
# Return a new list from a container, number sorted
sorted([10, 1, 3.6, 7, 5, 2, -3])
[-3, 1, 2, 3.6, 5, 7, 10]
# Return a new list from a container, strings sorted
# Note: Capitalized strings come first
sorted(['dogs', 'cats', 'zebras', 'Chicago', 'California', 'ants', 'mice'])
['California', 'Chicago', 'ants', 'cats', 'dogs', 'mice', 'zebras']
# Return a bug for mixed type 'str' and 'int' :  you cannot mix those
sorted([3, 'cats', 'zebras', 5, 'California', 'ants', 'mice'])
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-79-8ebd0f4060aa> in <module>()
      1 # Return a bug for mixed type 'str' and 'int' :  you cannot mix those
----> 2 sorted([3, 'cats', 'zebras', 5, 'California', 'ants', 'mice'])


TypeError: '<' not supported between instances of 'str' and 'int'
# Sum of numbers
sum([10, 1, 3.6, 7, 5, 2, -3])
25.6
# Smallest number
min([10, 1, 3.6, 7, 5, 2, -3])
-3
# First with alphabet order
min(['g', 'z', 'a', 'y'])
'a'
# Largest number
max([10, 1, 3.6, 7, 5, 2, -3])
10
# Largest item in a string container: returns the last letter in the alphabet order
max('gibberish')
's'
# Absolute value of a number
print ( abs(10) )
print ( abs(-12) )
10
12
# Use the repr() function to return a string representation of an object
repr(set1)
"{False, 3, 5, 6, 'cat', 'dog'}"

Python object attributes

How to access an attribute of an object: obj.attribute

Object attribute which is a callable, is called a method. Same as a function, but bounded to an object.

Object attribute which isn’t a callable, is called a property. It’s just a piece of data about the object, that is itself another object.

The built-in function dir() returns a list of an object’s attributes.


a_string = 'tHis is a sTriNg'

# list all atributes, including pre-defined methods from python like __method__
dir(a_string)
['__add__',
 '__class__',
 '__contains__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__getnewargs__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__mod__',
 '__mul__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__rmod__',
 '__rmul__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'capitalize',
 'casefold',
 'center',
 'count',
 'encode',
 'endswith',
 'expandtabs',
 'find',
 'format',
 'format_map',
 'index',
 'isalnum',
 'isalpha',
 'isdecimal',
 'isdigit',
 'isidentifier',
 'islower',
 'isnumeric',
 'isprintable',
 'isspace',
 'istitle',
 'isupper',
 'join',
 'ljust',
 'lower',
 'lstrip',
 'maketrans',
 'partition',
 'replace',
 'rfind',
 'rindex',
 'rjust',
 'rpartition',
 'rsplit',
 'rstrip',
 'split',
 'splitlines',
 'startswith',
 'strip',
 'swapcase',
 'title',
 'translate',
 'upper',
 'zfill']

__method__: the double underscore methods are special method names used by Python.
As far as one’s concerned, this is just a convention. It’s a way for the Python system to use names that won’t conflict with user-defined names. You will typically override these methods. For example, you often override the init method when writing a class.

Some methods on string objects

# Return a capitalized version of the string
a_string.capitalize()
'This is a string'
# Return an uppercase version of the string
a_string.upper()
'THIS IS A STRING'
# Return a lowercase version of the string
a_string.lower()
'this is a string'
# Notice that the methods called have not actually modified the string
a_string
'tHis is a sTriNg'
# Count number of occurences of a substring in the string
a_string.count('i')
3
# Count number of occurences of a substring in the string after a certain position
a_string.count('i', 7)
1
# Count number of occurences of a substring in the string
a_string.count('is')
2
# Does the string start with 'this'?
a_string.startswith('this')
False
# Does the lowercase string start with 'this'?
a_string.lower().startswith('this')
True
# Does the string end with 'Ng'?
a_string.endswith('Ng')
True
# Return a version of the string with a substring replaced with something else
a_string.replace('is', 'XYZ')
'tHXYZ XYZ a sTriNg'
a_string
'tHis is a sTriNg'
# Return a version of the string with a substring replaced with something else
a_string.replace('i', '!')
'tH!s !s a sTr!Ng'
# Return a version of the string with the first 2 occurences a substring replaced with something else
a_string.replace('i', '!', 2)
'tH!s !s a sTriNg'

string manipulations with format:

# format function
print('We are the {} who say "{}!"'.format('knights', 'hi'))

print('{1} and {0}'.format('spam', 'eggs'))
print('{0} and {1}'.format('spam', 'eggs'))

print('This {food} is {adjective}.'.format(food='spam', adjective='absolutely horrible'))
We are the knights who say "hi!"
eggs and spam
spam and eggs
This spam is absolutely horrible.
# '!a' (apply ascii()), '!s' (apply str()) and '!r' (apply repr()) can be used to convert the value
contents = 'eels'
print('My hovercraft is full of {}.'.format(contents))
print('My hovercraft is full of {!r}.'.format(contents))
My hovercraft is full of eels.
My hovercraft is full of 'eels'.
# ':' + format allows greater control over how the value is formatted. Here, only three places after the decimal.
import math
print('The value of PI is approximately {0:.3f}.'.format(math.pi))

# print('The value of PI is approximately %5.3f.' % math.pi) is deprecated in python 3
The value of PI is approximately 3.142.

Some methods on list objects

a_list = ['string1',45,100,'another string',1000]
type(a_list)
list
# note that is actually modifies the original object, and does not return a copy
a_list.remove(100)
a_list
['string1', 45, 'another string', 1000]
a_list.pop()
1000
a_list
['string1', 45, 'another string']

Some methods on set objects

a_set = {'string1',45,100,'another string',1000}
type(a_set)
set
a_set.update(['newItem1','newIte2'])
a_set
{100, 1000, 45, 'another string', 'newIte2', 'newItem1', 'string1'}
a_set.issuperset({45})
True
dir(a_set)
['__and__',
 '__class__',
 '__contains__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__iand__',
 '__init__',
 '__init_subclass__',
 '__ior__',
 '__isub__',
 '__iter__',
 '__ixor__',
 '__le__',
 '__len__',
 '__lt__',
 '__ne__',
 '__new__',
 '__or__',
 '__rand__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__ror__',
 '__rsub__',
 '__rxor__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__sub__',
 '__subclasshook__',
 '__xor__',
 'add',
 'clear',
 'copy',
 'difference',
 'difference_update',
 'discard',
 'intersection',
 'intersection_update',
 'isdisjoint',
 'issubset',
 'issuperset',
 'pop',
 'remove',
 'symmetric_difference',
 'symmetric_difference_update',
 'union',
 'update']

Some methods on dict objects

a_dict = {'key1': 'value1','key2': 'value2','key3': 'value3'}
a_dict.keys()
dict_keys(['key1', 'key2', 'key3'])
a_dict.values()
dict_values(['value1', 'value2', 'value3'])
a_dict.get('key2')
'value2'
a_dict['key2'] = 'value2_modified'
a_dict
{'key1': 'value1', 'key2': 'value2_modified', 'key3': 'value3'}

Arguments : Positional VS keyword

positional arguments: you must provide them in the order that the function defined them
keyword arguments: you provide the arguments in any order, as long as you specify each argument’s name

You can call a function or a method like this:

Important: when using positional & keyword arguments, positional arguments must come first.

def sum(arg1,arg2):
    return arg1 / arg2
sum(1,2)
0.5
# changing the order changes the result
sum(2,1)
2.0
def sum(kwarg1, kwarg2):
    return kwarg1 / kwarg2
sum(kwarg1 = 2, kwarg2 = 1)
2.0
# changing the order does not changes the result
sum(kwarg2 = 1, kwarg1 = 2)
2.0
sum(kwarg2 = 2, kwarg1 = 1)
0.5

Loops

for

# Note: xrange no longer exist in python 3
for x in range(0, 3):
    print( "iteration number %d" % (x) )
iteration number 0
iteration number 1
iteration number 2

Note :

if you need the index, use this code as explained here

for index, val in enumerate( range(0, 3) ):
    print(index, val)

for Else

# exits with something
for x in range(0, 3):
    print( "iteration number %d" % (x) )
else:
    print('end %d' % x)
iteration number 0
iteration number 1
iteration number 2
end 2

for in

collection = ['hey', 5, 'd']
for x in collection:
    print(x)
hey
5
d

for in - sub List

list_of_lists = [ [1, 2, 3], [4, 5, 6], [7, 8, 9]]
for list in list_of_lists:
    for x in list:
        print(x)
    else:
        print('end of sublist at x = %d' % x)
1
2
3
end of sublist at x = 3
4
5
6
end of sublist at x = 6
7
8
9
end of sublist at x = 9

while

x = 1
while x < 5:
    print ("iteration number %d" % (x))
    x += 1
iteration number 1
iteration number 2
iteration number 3
iteration number 4

Convert objects

The basic types and containers we have used so far all provide type constructors:

To convert to another type, use the type constructor for the type you want, and pass in the object you have.

a_num = 32
str(a_num)
'32'
a_string = 'string'
# will fail because this conversion can't be done
int(a_string)
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

<ipython-input-133-1d971295ef76> in <module>()
      1 # will fail because this conversion can't be done
----> 2 int(a_string)


ValueError: invalid literal for int() with base 10: 'string'

Classes: Always CamelCase

Definition and Instanciation

# Define a new class called `MyClass`
class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'
# gets i attribute from `MyClass`
MyClass.i
12345
# gets another attribute from `MyClass`
MyClass.__doc__
'A simple example class'

Now, we will create an instance of a MyClass and assigns this object to the local variable a_variable that has all attributes of its constructor

a_variable = MyClass()
a_variable.i
12345

Many classes like to create objects with instances customized to a specific initial state.
Therefore a class may define a special method named init(), like this:

class MyClassWithInit:
     def __init__(self, init_number):
         self.init_number = init_number

x = MyClassWithInit(3.0)
x.init_number
3.0

Pay attention: if you define a list shared among the class you may end with something like this

class Dog:
    tricks = []             # Shared

    def add_trick(self, trick):
        self.tricks.append(trick)

d = Dog()
e = Dog()

d.add_trick('roll over')
e.add_trick('play dead')

d.tricks
['roll over', 'play dead']

Solution: pass the tricks list in the init, so that it isn’t shared among all Dog Classes

class DogUnique:
    def __init__(self):
        self.tricks = []   # unique to each instance

    def add_trick(self, trick):
        self.tricks.append(trick)

d_unique = DogUnique()
e_unique = DogUnique()

d_unique.add_trick('roll over')
e_unique.add_trick('play dead')

d_unique.tricks
['roll over']
# Define a new class called `Thing` that is derived from the base Python object
class Thing(object):
    my_property = 'I am a "Thing"'


# Define a new class called `DictThing` that is derived from the `dict` type
class DictThing(dict):
    my_property = 'I am a "DictThing"'
print(Thing)
print(type(Thing))
print(DictThing)
print(type(DictThing))
print(issubclass(DictThing, dict))
print(issubclass(DictThing, object))
<class '__main__.Thing'>
<class 'type'>
<class '__main__.DictThing'>
<class 'type'>
True
True
# Create "instances" of our new classes
t = Thing()
d = DictThing()
print(t)
print(type(t))
print(d)
print(type(d))
<__main__.Thing object at 0x10a0d0b38>
<class '__main__.Thing'>
{}
<class '__main__.DictThing'>
# Interact with a DictThing instance just as you would a normal dictionary
d['name'] = 'Sally'
print(d)
{'name': 'Sally'}
d.update({
        'age': 13,
        'fav_foods': ['pizza', 'sushi', 'pad thai', 'waffles'],
        'fav_color': 'green',
    })
print(d)
{'name': 'Sally', 'age': 13, 'fav_foods': ['pizza', 'sushi', 'pad thai', 'waffles'], 'fav_color': 'green'}
print(d.my_property)
I am a "DictThing"

Remarks on Classes

Data attributes override method attributes with the same name Capitalizing method names is good practise Prefixing data attribute names with an underscore is good practise Warning: Clients may mess up invariants data maintained by the methods, by stamping on their data attributes

Often, the first argument of a method is called self. This is nothing more than a convention: the name self has absolutely no special meaning to Python. Note, however, that by not following the convention your code may be less readable to other Python programmers, and it is also conceivable that a class browser program might be written that relies upon such a convention.

# Multiple Inheritance
class DerivedClassName(dict, object):
    def __init__(self,name):
        self.name = name
dir(dict)
['__class__',
 '__contains__',
 '__delattr__',
 '__delitem__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__setitem__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'clear',
 'copy',
 'fromkeys',
 'get',
 'items',
 'keys',
 'pop',
 'popitem',
 'setdefault',
 'update',
 'values']
dir(DerivedClassName)
['__class__',
 '__contains__',
 '__delattr__',
 '__delitem__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__setitem__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'clear',
 'copy',
 'fromkeys',
 'get',
 'items',
 'keys',
 'pop',
 'popitem',
 'setdefault',
 'update',
 'values']

Scope of variables

**nothing = local: ** inside the function only
**nonlocal: ** inside the upper function/class
**global: ** inside the hole module

spam = 'nothing'
def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "initial spam"
    do_local()
    print("After do_local:", spam)
    do_nonlocal()
    print("After do_nonlocal:", spam)
    do_global()
    print("After do_global:", spam)

print("Spam at the begining:", spam, "\n")
scope_test()
print("\nIn global scope:", spam)
Spam at the begining: nothing

After do_local: initial spam
After do_nonlocal: nonlocal spam
After do_global: nonlocal spam

In global scope: global spam

Modules

Enter the Python interpreter: open your terminal, type python

If you quit from the Python interpreter and enter it again, the definitions you have made are lost. Therefore, you are better off creating a script. As your program gets longer, you may want to split it into several files. Such a file is called a module. Definitions from a module can be imported into other modules like this

  1. Create and save a file as testfile.py

    def test(): return 34

  2. In the Python interpreter, type

    import testfile
    testfile.test()

Other notation possibilities:
import testfile as file
from testfile import test
from testfile import test as testfunction

Know what modules are defined

dir(testfile)

execute a module as a script from the terminal:

python testfile.py

import modules from a package / sub-package:

import package.sub_package.testfile_2

Reading & Writing Files

# arguments for open (filename, mode)
#'r' read (default mode)
#'w' writing (an existing file with the same name will be erased)
#'a' opens for appending
#'r+' opens for both reading and writing

f = open('workingfile', 'w')
f.close()

The default when reading is to convert platform-specific line endings (\n on Unix, \r\n on Windows) to just \n.
When writing in text mode, the default is to convert occurrences of \n back to platform-specific line endings.

This behind-the-scenes modification to file data is fine for text files, but will corrupt binary data like that in JPEG or EXE files. Be very careful to use binary mode when reading and writing such files.

# enter the binary mode with 'b'
f = open('workingfile', 'wb')

print(f.closed)

f.close()

print(f.closed)
# using a with automatically closes at the end -> Good Practise !
with open('workingfile', 'r') as f:
    read_data = f.read()

#is the connection closed ?
f.closed

Writing

f.read(size) -> reads some quantity of data. When size is omitted or negative, the entire contents of the file will be read and returned; it’s your problem if the file is larger than your machine’s memory.

with open('workingfile', 'w') as f:
    f.write('This is a test line\n and a second line')

Reading

# let's see what we have in the file now
with open('workingfile', 'r') as f:
    for line in f:
        print(line, end='')

Useful functions:

JSON Read & write

a_json = {"one" : "1", "two" : "2", "three" : "3"}
# see the stringified version of your json
import json
json.dumps(a_json)
# write your json into the file workingfile
with open('workingfile', 'w') as f:
    json.dump(a_json,f)
# and read the file
with open('workingfile', 'r') as f:
    for line in f:
        print(line, end='')
# and retrieve the JSON
with open('workingfile', 'r') as f:
    the_json = json.load(f)
the_json

Errors

Handle errors

# Handle an error
try:
    3 / 0
except Exception as e:
    print('cannot divide, for any error it may be',e)
else:
    print('divided')
cannot divide, for any error it may be
try:
    3 / 1
except (RuntimeError, TypeError, NameError):
    print('the error was of kind RuntimeError, TypeError, NameError')
except (ZeroDivisionError):
    print('the error was of kind ZeroDivisionError')
else:
    print('no errors, that\'s good')
no errors, that's good
try:
    3 / 0
except (RuntimeError, TypeError, NameError):
    print('the error was of kind RuntimeError, TypeError, NameError')
except (ZeroDivisionError):
    print('the error was of kind ZeroDivisionError')
else:
    print('no errors, that\' good')
the error was of kind ZeroDivisionError

Whatever ending result

try:
    3 / 0
except Exception as e:
    print('the error was raised here')
else:
    print('no errors, that\' good')
finally:
    print('Always printed!')
the error was raised here
Always printed!

Volunatry error

raise NameError('HiThere')
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-155-72c183edb298> in <module>()
----> 1 raise NameError('HiThere')


NameError: HiThere

Good Practise

References