Python

Python Tutorial for Beginners

Let’s talk about Python. If you want to learn Python from scratch, this tutorial will be a good read for you.

By using Python 3.4.3 (which you can download here), we’ll see the basics of this awesome language.

This tutorial expects you to have some basic knowledge of programming. And knowing any other language will be a big plus.

1. What is Python?

Python is a high-level, object oriented, interpreted programming language. It was created by Guido van Rossum, and its source code is available under the GNU General Public License.

It’s a great language for the beginner-level programmers and supports the development of a wide range of applications from simple text processing to big web applications and even games.

This language has a lot of awesome features:

  • It’s easy to learn, as there are few keywords and it has a simple structure and a clearly defined syntax.
  • It’s easy to read, as Python code is more clearly defined and visible to the eyes.
  • Its code is fairly easy to maintain.
  • It has a broad standard library that is very portable and cross-platform compatible with UNIX, Windows and Macintosh.
  • It has an interactive mode which allows interactive testing and debugging of snippets of code.
  • It can run on a wide variety of hardware platforms and has the same interface on all platforms.
  • You can add low-level modules to the Python interpreter. These modules enable programmers to add to or customize their tools to be more efficient.
  • Python provides interfaces to all major commercial databases.
  • Python supports GUI applications that can be created and ported to many system calls, libraries and windows systems, such as Windows MFC, Macintosh, and the X Window system of Unix.
  • Python provides a better structure and support for large programs than shell scripting.

Also, it supports functional programming, which is a huge plus for me at least, and it can be used as a scripting language or be compiled to build big applications too.

It provides high-level dynamic data types and supports dynamic type checking. It also supports automatic garbage collection.

Python has a lot of implementations, but the default and most used across all platforms is CPython, as this is the one you download from python.org. It’s the Python interpreter most used right now, despite one big issue it has: The Global Interpreter Lock (GIL).

The GIL is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. This means that, although you can do multi-threading with Python, the threads will not actually run in parallel, but synchronized instead. This makes Python (when running on CPython implementation) a single-thread programming language.

Enough with the introduction to Python, if you are still reading it means that you are actually interested in this language so, let’s get on with it.

2. The code

The code in python is mainly written in *.py files. Let’s give it a try writing a hello world application.

hello_world.py

print("Hello World!")

We can run this program by executing on bash the command: python3 hello_world.py. We’ll see:

$ python3 hello_world.py
Hello World!

But this is not the only way to run Python scripts, if you add the shebang line and give the file execution permissions, like this:

hello_world

#!/usr/bin/python3

print("Hello World!")

You can do something like this:

$ ./hello_world
Hello World!

So, there it is. You’ve written some Python.

3. Variables

Now, in almost every program, you need to store values somewhere for later processing. You may have some process that returns an exit code, and you want to do some stuff according to the resulting exit code. In that case you surely want to save that exit code in a variable.

Variables are reserved memory locations to store values, that means that whenever you define a variable you reserve some memory for whatever you are going to store in it.

In Python, you don’t need to explicitly define variables to reserve the memory. The declaration happens automatically when you assign a value to it with the equals sign (=).

So, basically, when you are doing variable = value, you are doing two things there. You are reserving memory enough to store the value, and you are storing the value there. Let’s see it on practice:

variables.py

my_integer = 1000
my_decimal = 10.9
my_string = "Hi!"

print(my_integer)
print(my_decimal)
print(my_string)

Here, we are defining three variables. An integer, a decimal and a string. As you can see, we are not explicitly telling the interpreter the type of the variables, as it dynamically infers it from the values.

Then we are printing each of them, and when we run this script will see:

python3 variables.py
1000
10.9
Hi!

I’ll use this opportunity to introduce you to a very useful feature Python (but not only Python) has, built-in functions.

Built-In Functions are functions provided by the language that are available everywhere. Throughout this example we’ll see some of the most used built-in functions. Here we’ll talk about type.

As Python has dynamic type inference, we sometimes can’t be sure of the type of a variable by just seeing the code. Passing a variable as argument to the type function, it will return the class of the provided variable. Let’s see:

variables.py

my_integer = 1000
my_decimal = 10.9
my_string = "Hi!"

print(my_integer)
print(type(my_integer))

print(my_decimal)
print(type(my_decimal))

print(my_string)
print(type(my_string))

We are now printing the type of the variables below their values. When we run it:

python3 variables.py
1000
<class 'int'>
10.9
<class 'float'>
Hi!
<class 'str'>

There you see. Our integer variable type is int, the decimal’s is float and the string’s is str.

There are also built-in functions for casting purposes. When you want the string representation of an integer you can write str(my_integer_value), and it will output what you need. It works for all types (int, float, etc.).

Now, we have some understanding of variables. Let’s jump to…

4. Data Structures

Python provides some data structures that makes value storing a lot more easy. Available data structures are: lists, tuples, sets and dictionaries.

This section will make an introduction to all data structures in Python, but will see how to work with them in a more advanced way when we get to iterations and decisions.

4.1. Lists

Lists are a way to group together other values. A list is a sorted array of values, written as a list of coma separated values between two square brackets like:

my_list = [1, 2, 3, 4]

Of course, to define an empty list we just write [] (empty square brackets). Also, lists may contain items of different types as in ["a string", 1, 2.0], but usually the items will be all of the same type.

Of course, data structures can contain each other, so a list of lists is actually possible as in [[1,2,3], [3,4], ["asd", "qwe]].

Lists are indexed, to access an item within one we just need to write something like my_list[1], and this will return the second element of our list. Yes, the second element, lists’ indexes, in good languages, start at 0 (let the hate begin, but you are wrong if you think otherwise :P).

Also, to put an element into a list we can either assign it to the index as in my_list[1] = "a value", or we can append it as in my_list.append("a value"). The first option is often a bad idea, as the index must exist first to assign something to it, let’s see:

index_out_of_bounds.py

my_list = [1, 2, 3]

print(str(my_list))

my_list.append(4)

my_list[2] = 5

print(str(my_list))

my_list[4] = 2

Here we are defining a list of three elements and printing it. Then we append a fourth element, replace the third and print again. And then we are assigning something to the fifth element, BUT the fifth element does not yet exist in the array, let’s see what happens:

$ python3 index_out_of_bounds.py
[1, 2, 3]
[1, 2, 5, 4]
Traceback (most recent call last):
  File "index_out_of_bounds.py", line 11, in <module>
    my_list[4] = 2
IndexError: list assignment index out of range

That “IndexError: list assignment index out of range” is a Python exception, telling us that we are assigning an element to an index of the array that wasn’t defined yet. Which takes us to our first reserved words in Python: try-except.

Imagine we want to replace an element if it exists, but if it doesn’t we don’t want to do anything. There are better ways than the one explained below, but I want you to see how try-except works.

replaces_if_exists.py

my_array = [1, 2, 3]

try:
    my_array[3] = 4
except IndexError:
    print("element did not exist. ignoring.")

Here we define a list of three elements again, and want to replace the fourth (which does not yet exist). If an IndexError is thrown, the except clause will execute the block of code within it. Let’s see:

$ python3 replaces_if_exists.py
element did not exist. ignoring.

That is one way of doing error handling in Python.

Now, you also may want to delete an item of a list, and here we meet another keyword: del. Let’s imagine we need to remove the las element of an array, but we don’t know its length. Here comes another built-in function: len.

remove_last_element.py

my_list = [1, 2, 3]

del my_list[len(my_list) - 1]

print(my_list)

Here we define a list with three items in it, but we treat it as if we didn’t know its length. By using len(my_list) we get its length (three), so by subtracting one we get the last index (two). Applying del to that index of our list will remove it. When we run this script:

python3 remove_last_element.py
[1, 2]

Now we know how to define arrays, add items to them, retrieve those items, delete them and even get their length. Of course, more advanced ways of working with them will be described later. Let’s skip to the next data structure.

4.2. Tuples

Tuples are a very simple, yet useful, data structure in python. They behave a lot like lists, but they are immutable, which means that they can not be modified after declaration. They also support nesting (a tuple can be present within another tuple).

Tuples are defined almost like a list, but without the brackets as in my_tuple = 1, 2, "a string", 4.5. They are also indexed, which means that providing an index within square brackets will return an element in that position (as always, the first index is zero). The built-in function len is also available here.

Let’s see a little example:

tuples_basic.py

my_tuple = 1, "a string", 3.5

print(str(my_tuple[0]))

try:
    del my_tuple[1]
except TypeError:
    print("tuples are immutable. can't remove")

try:
    my_tuple[1] = 3
except TypeError:
    print("tuples are immutable. can't assign")

print(str(my_tuple))

Here we define a tuple of three elements, print the first one, delete the second and replacing the second. The last two operations are within a try-except to show you that tuples are immutable. Then we print the whole tuple.

$ python3 tuples/tuples_basic.py
1
tuples are immutable. can't remove
tuples are immutable. can't assign
(1, 'a string', 3.5)

Of course, as in lists, tuples maintain the order of the elements in which they were defined. You might think that this is pretty obvious for any data structure but we will see that it isn’t.

4.3. Sets

A set is an unordered collection with no duplicate elements. As it is unordered, it is not indexed, which means that the access and assignment we saw with lists will not be available with sets. Basic uses include membership testing and eliminating duplicate entries. Set objects also support mathematical operations like union, intersection, difference, and symmetric difference.

Curly braces or the set() function can be used to create sets. To create an empty set you have to use set(), not {}; the latter creates an empty dictionary, a data structure that we discuss below.

Let’s see a simple example on sets:

tuples_basic.py

my_set = {"a string", 2, "another string"}
print(str(my_set))

my_set.add(3.5)
print(str(my_set))

print(str(my_set.pop()))
print(str(my_set))

print(str(my_set.pop()))
print(str(my_set))

print(str(my_set.pop()))
print(str(my_set))

print(str(my_set.pop()))
print(str(my_set))

In this script we are defining a set, adding an element, and then popping out elements. The method pop in sets, returns an element, removing it from the set. We are printing the set after every operation. The output:

$ python3 sets/sets_basic.py
{'a string', 2, 'another string'}
{'a string', 2, 3.5, 'another string'}
a string
{2, 3.5, 'another string'}
2
{3.5, 'another string'}
3.5
{'another string'}
another string
set()

Here we can see a couple of things going on. First, to add an element to the set we don’t use append as in our lists, we use add, and as we see, the element is added in any position, not the last nor the first.

Now we’ll talk about the operations we said it supports earlier: union, intersection, difference, and symmetric difference.

  • Union between two sets should return another set containing all the elements that are present in those sets. In other words, union between a set a containing {1,2,3} and a set b containing {3,4,5} should return another set containing {1,2,3,4,5}.
  • Intersection between two sets should return another set containing all the elements that are present in both involved sets. In other words, intersection between a set a containing {1,2,3} and a set b containing {3,4,5} should return another set containing {3}.
  • Difference between two sets should return a set containing all the elements that are present in the first but not in the second. In other words, difference between a set a containing {1,2,3} and a set b containing {3,4,5} should return another set containing {1,2}.
  • Symmetric difference between two sets should return a set containing all the elements that are not present in both sets. In other words, symmetric difference between a set a containing {1,2,3} and a set b containing {3,4,5} should return another set containing {1,2,4,5}.

Now, let’s talk about a couple things all these data structures support before going on with an example. The keyword in and the fact that strings are, in fact, some kind of data structure.

The keyword in provides a way of seeing if an item is present within some of these data structures. If we define an array like my_list = [1,2,3] and then evaluate the expression 2 in my_list, we’ll get the boolean value True. It’s an easy and convenient way of checking whether an element is in an array or not.

Another thing to notice, is the resemblance of the behavior of string and arrays. As in any other language (at least every one I know of), string are actually arrays of chars. This means that the built-in function len and the keyword in also works in string. Also, strings are indexed sequences, so "hello"[2] will work and return 'l'.

Now we’ll see some weird things, but I’ll explain them all after the example.

tuples_basic.py

a = set("abcdef")
b = {"b", "d", "g", "h"}

print("a: " + str(a))
print("b: " + str(b))

union = a.union(b)
intersection = a.intersection(b)
difference = a.difference(b)
symmetric_difference = a.symmetric_difference(b)

print("union: " + str(union))
print("intersection: " + str(intersection))
print("difference: " + str(difference))
print("symmetric difference: " + str(symmetric_difference))

print("a in union: " + str("a" in union))
print("b in difference: " + str("b" in difference))

The output:

$ python3 sets/set_operations.py
a: {'d', 'b', 'f', 'c', 'e', 'a'}
b: {'g', 'h', 'd', 'b'}
union: {'g', 'b', 'f', 'c', 'e', 'd', 'h', 'a'}
intersection: {'d', 'b'}
difference: {'e', 'f', 'a', 'c'}
symmetric difference: {'g', 'f', 'c', 'e', 'h', 'a'}
a in union: True
b in difference: False

Now, what is going on there? We are instantiating two sets. One using the built in function set, which receives any of the data structures mentioned here (and more) as argument and returns a set with the contained elements. This built-in function belongs to a group of functions that instantiate these data structures from another data structure, like list which receives any iterable and builds a list with its content.

The second set is being built with the literal (values separated by commas within curly braces). Then we are printing them, doing some operations and printing the results too.

Also, we are checking if the strings “a” and “b” are present in the union and difference respectively. As a result you can see that True is returned for "a" in union and False is being returned for "b" in difference. Here you see, besides the use of the in keyword, an example of boolean values in Python.

Keep in mind that any value can be tested for a boolean value. Every value in the below list, will evaluate to false:

  • None (The null of Python if you want)
  • False (The False boolean value)
  • Zero (For any numeric type, for example, 0, 0.00, etc.)
  • Any empty sequence ('', [], {}, etc.)
  • Instances of user-defined classes, if the class defines a __bool__() or __len__() method, when that method returns the integer zero or bool value False (We’ll talk about this in a couple sections).

Now we have a good understanding on sets, let’s move on to…

4.4. Dictionaries

Dictionaries in Python are data structures that optimize element lookups by associating keys to values. You could say that dictionaries are arrays of (key, value) pairs, where the keys can be of any immutable type (strings, numbers, etc.).

The syntax is somewhat familiar to any one that has worked with REST services or JavaScript applications, as they look a lot like JSON. An object starts and ends in curly braces, each pair is separated by a comma, and the (key, value) pairs are associated through a colon (:). Let’s see an example:

dictionary_def.py

my_dictionary = {
    "a key": "a value",
    "another key with numeric value": 3.6,
    True: "A boolean as a key",
    5: "An int as a key"
}
print(str(my_dictionary))

See? It’s a JSON. And the string representation looks the same when we print it, as in any other sequence:

$ python3 dictionaries/dictionary_def.py
{True: 'A boolean as a key', 'a key': 'a value', 5: 'An int as a key', 'another key with numeric value': 3.6}

Now, the keys of a dictionary are not ordered. We’ll make a little experiment when we get to iterations. Also, they are unique, a key can not be present twice in the same dictionary. If we open the intepreter running $ python3 in our shell we can do something like:

$ python3
Python 3.4.3 (default, Oct 14 2015, 20:28:29)
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = {"key": "value", "key": "value2"}
>>> print(str(a))
{'key': 'value2'}

On the result, the key 'key' is only present one with the last value we assigned to it.

Now, we’ve got two ways of accessing dictionaries in Python: using square brackets providing the key, or using the method get. The later is the best approach most of the times, let’s talk about why.

access.py

my_dict = {
    "name": "Sebastian",
    "age": 21
}

print(str(my_dict.get("name")))
print(str(my_dict["name"]))
print(str(my_dict.get("key that does not exist")))
print(str(my_dict["key that does not exist"]))

We are creating a dictionary with my name and my age, and then retrieving the name using both of the ways we talked about. This will work just fine as the key is present in our dictionary, but when we try to access a key that does not exist, well let’s see the output:

$ python3 dictionaries/access.py
Sebastian
Sebastian
None
Traceback (most recent call last):
  File "dictionaries/access.py", line 9, in <module>
    print(str(my_dict["key that does not exist"]))
KeyError: 'key that does not exist'

A KeyError is raised when we try to gain access to a key that doesn’t exist with the square brackets. The line before the error says None, in my opinion every alternative that does not raise an error is better.

In programming I think that errors should be treated as that, as errors, we should not use them to control the flow of an application but as exceptions that should not happen and we should recover from them.

If we know a key might not be present when treating with a dictionary, I think we should use None to represent an absent key, instead of an ugly KeyError. Again, that’s just how I think.

Dictionaries are mutable, that means that we can modify them. We can replace values assigned to keys, delete key-value pairs and add new ones. The replacement and addition are made the same way, by assigning the value to a key within square brackets, like my_dict["key"] = "new value". If a “key” exists, then replaces its value with “new value”, else it creates it and assigns it.

To delete a key we use, as in lists, the keyword del, as in del my_dickt["key"]. Let’s see an example of this:

write_dictionary.py

my_dog = {
    "breed": "German Shepperd",
    "name": "Qwerty",
    "age": 9,
    "gender": "MALE"
}

print(str(my_dog))

my_dog["age"] = my_dog.get("age", 0) + 1
my_dog["girlfriend_name"] = "Quiara"

print(str(my_dog))

del my_dog["girlfriend_name"]

print(str(my_dog))

Here we are defining… my dog, and then printing it. Then he turned ten and got a girlfriend, and then his girlfriend moved to another city.

What is really happening is that we are defining a dictionary, overriding a key (adding one to it if it exists), creating another key and then deleting it. Here is an example of how to operate with numbers, which we’ll see more in detail soon.

Where we add one to the age, we are providing a second argument to the method get, that is a default value if the key does not exist. In this case this will not happen, but I wanted you to see that this could be done.

Now, the keyword in works with dictionaries too, it checks if a key exists. In the previous example, the default age thing could be implemented using it, let’s modify the script to do it:

write_dictionary.py

my_dog = {
    "breed": "German Shepperd",
    "name": "Qwerty",
    "age": 9,
    "gender": "MALE"
}

print(str(my_dog))

if "age" in my_dog:
    my_dog["age"] += 1
else:
    my_dog["age"] = 1

my_dog["girlfriend_name"] = "Quiara"

print(str(my_dog))

del my_dog["girlfriend_name"]

print(str(my_dog))

Hey! You’ve just saw an if-else there! This is an easy way of safely using the square brackets notation, but let’s explain the if-else before we dive into it. The syntax is pretty simple:

if condition:
    action
elif another condition:
    another action
elif another condition:
    another action
...
else:
    action if none of the above conditions is met

We’ll see how to make decisions deeper later. So, in the example we are checking if age exists, if it does we add one and if it doesn’t we assign one to it. Pretty simple, it’s kind of the same thing the method get does for us.

The len built-in function also works with dictionaries, it will return the amount of key-value pairs it contains. It also provides some methods that, later, will help us iterate over dictionaries more easily.

This is all about data structures for now, we’ll soon jump into flow control tools so we can make something more interesting with python, but before we jump into it (I know, but we need to), we have to talk a little bit about operators and dig deeper into Python types.

5. Python Types and Operators

In this section we’ll talk about, mainly, three data types: boolean, string and number.

As we said before, we never tell Python the type of a variable (at least not explicitly), it infers it from the value we are assigning to it. When we assign 1 to a variable, Python automatically defines that variable as int, as it is the simplest type that can contain the value you are trying to assign. But, although the language is able to tell the types you are handling, you should too, as it will help you debug your code more easily.

5.1. Booleans (bool)

Booleans, or bool values, are logical values that can be either True or False. They are immutable and support some useful operators:

  • and: Given two bool values a and b, the expression a and b will evaluate to True if both of them are true. It will be False otherwise.
  • or: Given two bool values a and b, the expression a or b will evaluate to True if at least one of them is true. It will be False otherwise.
  • not: Given a bool value a, the expression not a will evaluate to True if a is False. It will be False otherwise.

Let’s do a couple of exercises with the interpreter:

$ python3
Python 3.4.3 (default, Oct 14 2015, 20:28:29)
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = True
>>> b = False
>>> a and b
False
>>> a or b
True
>>> not a and b
False
>>> not (a and b)
True
>>>

So, here we have two bool values, a and b, and test our logical operators. As you see in the operations involving not, you can combine them, but you have to use a combination of parentheses to be sure that you are actually doing the operations you want to do.

Now, Python also provides comparators, which gives us a way of comparing values resulting in booleans. Available comparators are:

  • <: Strictly less than. (4 < 2 = False, 10 < 30 = True)
  • <=: Less than or equal. (4 <= 4 = True, 5 <= 9 = True, 5 <= 2 = False)
  • >: Strictly greater than. (4 > 2 = True, 4 > 5 = False)
  • >=: Greater than or equal. (4 >= 4 = True, 5 >= 9 = False, 5 >= 2 = True)
  • ==: Equals. (4 == 4 = True, 5 == 4 = False)
  • !=: Not Equals. (4 != 4 = False, 5 != 4 = True)
  • is: Object identity. (4 is 4 = True, 4 is 5 = False, 4 is None = False)
  • is not: Negated object identity. (4 is not 4 = False, 4 is not 5 = True, 4 is not None = True)

With this comparators and the logical operators we can do now pretty useful stuff. Let’s see an example:

comparators.py

a = 5
b = 3
c = 2

# True
print(a > b > c)
print(a >= c)
print(a > b > c < a)
print(a == b or c < b)
print(not (a == b and c >= a))
print(b != c)
print(a is not None)

print("-----------------")

# False
print(a < b < c)
print(b > 10)
print(b is None and a != 5)
print(not a)

The output:

$ python3 data-types/comparators.py
True
True
True
True
True
True
True
-----------------
False
False
False
False

Here you see stuff like a > b > c < a, comparators have all the same priority, so this is like a > b and b > c and c < a. The other operations are just combinations of the stuff we saw recently. Let’s jump into numbers now.

5.2. Numbers (int, float, long, complex)

There are four distinct numeric types: plain integers (int), long integers (long), floating point numbers (float), and complex numbers (complex).

Python provides operators that are very similar to the ones available in other languages. Sum (+), difference (-), product (*), quotient (/), floored quotient (//), remainder (%) and power (**). There are also some built-in functions that let us do some other operations, like abs which returns the absolute value of magnitude of a number provided as argument, or divmod, which receives two numbers, a and b, and returns a pair containing a // b and a % b.

Let’s see some operations at work:

operations.py

a = 10
b = 7

sum = a + b
diff = a - b
product = a * b
quotient = a / b
floored_quotient = a // b
remainder = a % b

print(sum)
print(diff)
print(product)
print(quotient)
print(floored_quotient)
print(remainder)

print(abs(b - a))
print(int(a / b))
print(complex(0.3, 5) - complex(1, 2))

Here we see some of the operations we talked about, in the output we can see the results:

$ python3 data-types/operations.py
17
3
70
1.4285714285714286
1
3
3
1
(-0.7+3j)

The operators in python respect the priorities we learnt in math, as in a + b * c behaves as a + (b * c). Now, let’s talk about strings.

5.3. Strings (str)

As we talked before, string are arrays of characters. They behave a lot like lists, but they are immutable. This means that doing "hel" in "hello" will work and will return true, that doing "hello"[1] will work and return “e” and that doing len("hello") will work and return 5.

Let’s do some operations with strings so we can see how they behave:

string_ops.py

my_string = "Hello World!!"

print(my_string)
print(len(my_string))
print("He" in my_string)
print(my_string[3])
print(my_string[1:4])
print(my_string[1:4] + my_string[6:8])

Let’s explain this a little bit before seeing the output. We defined a string and printing it. Then we print its length, the result of checking if it contains the string “He” and the character in the index 3.

The other two lines are slices, they work in any indexed sequence. Between square brackets you define a slice array[n:m] and you will receive an array that contains the element present in that interval. A thing to notice is that the interval is [n;m), open at the end.

The last operation we do is the concatenation of two slices of the string. Let’s see the output:

$ python3 data-types/string_ops.py
Hello World!!
13
True
l
ell
ellWo

That’s how you work with strings in Python. Now let’s jump into those flow control tools.

6. Flow Control Tools

Flow control tools are tools that Python provides to make decisions and iterate through some data. The most common ones are while, for and if.

The while and for statements are tools to repeat executions.

The while loop is a conditional iteration. Its syntax is pretty simple:

while condition:
    action
else:
    some action

The loop will evaluate the provided condition executing the action until the condition is no longer met. If the condition is not met and the else clause is present, its body is executed and the while is terminated, of course, the else is optional. Let’s see:

while-else.py

i = 0
my_list = [1, 2, 3, 4, 5, 6]

while i < len(my_list):
    print(my_list[i])
    i += 1
else:
    print("no element is left to print")

Here we define an array and an int index initialized with 0, while that index is less than the array’s length, we print the respective element and add one to the index. When the condition is no longer met, we print a message. The output:

$ python3 flow-control/while-else.py
1
2
3
4
5
6
no element is left to print

Now, there are a few reserved words that come in handy some times with loops: break and continue. A break statement in a loop will terminate its execution without executing the else clause. A continue statement will skip the rest of the action and go back to testing the condition. Let’s modify our example to test these keywords:

while-else.py

i = 0
my_list = [1, 2, 3, 4, 5, 6]

while i < len(my_list):
    if my_list[i] == 5:
        i += 1
        break
    if my_list[i] % 2 == 0:
        i += 1
        continue
    print(my_list[i])
    i += 1
else:
    print("no element is left to print")

We just added two if statements. The first one will break if the actual element is 5, and the second one will skip this step if its an even number. The output will look like:

$ python3 flow-control/while-else.py
1
3

This script is only printing odd numbers until 5 appears. That’s a while loop. Another way we can achieve this is with a for loop.

The for loop is a non-conditional iteration statement. It is used to iterate over items in any iterable object. The syntax is beautiful:

for element in iterable:
    action
else:
    action

The iterable expression is only evaluated once, this means that if we have a function (we’ll talk about them later) that returns a sequence, we can put the call right in the for statement and be sure that it will be called only once, as the for will create an iterator of the result of evaluating that expression.

The action will be called for every element contained by the resulting iterator, in the order returned by it. When all the elements in that iterator are exhausted, the action in the else statement (if present) is executed and then the loop is terminated.

The break and continue keywords behave the same as in the while loop. Let’s rewrite our previous example to make it work with a for loop:

for-else.py

my_list = [1, 2, 3, 4, 5, 6]

for item in my_list:
    if item == 5:
        break
    if item % 2 == 0:
        continue
    print(item)
else:
    print("no element is left to print")

The output remains the same:

$ python3 flow-control/for-else.py
1
3

I don’t know what you are thinking now, but the for loop seems a lot more elegant for this case, as we don’t need to retrieve the element from the array in the body of the loop, the for does it for us. The while loop was not meant to be used to iterate over indexes, but to perform actions until a condition is met.

Let’s write an example that will actually do something interesting and shows us both iterations in the way they were meant to work: Let’s write a script that translates numbers from decimal to binary!

The thing here is that, we will work with 0 and positive numbers that are < 256, but we want the 8 bits to be displayed. For example, binary representation for the decimal “2” is “10”, but we want to see 8 bits, so “00000010” should be displayed. Let’s get to work:

binary_translator.py

my_numbers = [2, 125, 9, 255, 0]

for n in my_numbers:
    binary_representation = ""
    remain = n
    while remain >= 2:
        binary_representation = str(remain % 2) + binary_representation
        remain //= 2
    binary_representation = str(remain) + binary_representation
    binary_representation = ("0" * (8 - len(binary_representation))) + binary_representation
    print(binary_representation)

Here we see some magic. We have a list of numbers to translate, so we iterate though them with a for loop, then, while the subject number is greater or equal than 2 we append the remainder of n // 2 to the start of our binary_representation, then we change our subject to the result of that division.

Then we append what’s left of our subject to its representation and prepend as many zeros as we need to display 8 bits. Yes, the product (*) operation works with any iterable, [2] * 3 will return [2,2,2]. Now, let’s see the output of this script:

$ python3 flow-control/binary_translator.py
00000010
01111101
00001001
11111111
00000000

That was our first interesting exercise, see how Python is now starting to look good? Well, it gets better. Let’s go back to our previous example, the one with the odds number. One thing you may have noticed is that we wrote i += 1 three times in the body of our while loop. There is a better way to do it using the try statement we saw before.

The try statement does not only helps us handle errors, it also comes with a handy finally clause. The syntax of the try statement is very convenient:

try:
    action
except Error as identifier:
    on error action
except AnotherError as identifier:
    on another error action
finally:
    another action

The try statement will try to execute action, and if an error is raised it will look for an except clause that matches the error. The finally clause will execute always after the action was performed (or tried to) and any error has been handled. So, let’s rewrite our while-else example to use this useful statement:

while-else.py

i = 0
my_list = [1, 2, 3, 4, 5, 6]

while i < len(my_list):
    try:
        if my_list[i] == 5:
            break
        if my_list[i] % 2 == 0:
            continue
        print(my_list[i])
    finally:
        i += 1
else:
    print("no element is left to print")

Now it looks better. We don’t care if any condition is met in the body of our while, we always want the i to be incremented, so we put it in a finally statement and everything looks better now.

Now, another flow control tool we need to check before jumping into functions is the with statement. The with statement is used to wrap the execution of a block with methods defined by a context manager. This allows common try…except…finally usage patterns to be encapsulated for convenient reuse.

It’s a statement the comes handy when we have to handle closable resources as the with will automatically close them when it’s body is finished executing.

To explain the with statement we need to meet at least one of the resources that are compliant with it.

We’ll meet files, and more precisely, CSV files. CSV stands for comma separated values, and it is a standard for exporting and importing data across multiple formats.

It stores numbers and text in plain text. Each row of the file is a data record and every record consists of one or more fields which values are separated by commas. The use of the comma to separate every record’s fields is the source of the name given to this standard.

A CSV file, most of the times, consists of a row with the headers, which describes what does every column represent, and then every row after that is a set of data. Let’s see how we can read and write dictionaries to CSV files with Python:

with.py

import csv

my_dictionaries = [
    {
        "id": 0,
        "value": 123
    },
    {
        "id": 1,
        "value": 749
    },
    {
        "id": 2,
        "value": 913
    }
]

with open('/tmp/with_statement.csv', 'w+', newline='') as csv_file:
    head = my_dictionaries[0]
    keys = sorted([k for k in head.keys()])
    writer = csv.DictWriter(csv_file, fieldnames=keys)
    writer.writeheader()
    for d in my_dictionaries:
        writer.writerow(d)

with open('/tmp/with_statement.csv', 'r+', newline='') as csv_file:
    read_dictionaries = [d for d in csv.DictReader(csv_file)]

for d in read_dictionaries:
    print(str(d))

Well, there is a lot of new stuff in that script. Let’s see… The first line is an import, python provides a lot of libraries that gives us interfaces to do a lot of fun stuff, and there are even more libraries we can download from the internet and do even more fun stuff. These libraries have modules inside, and the import statement is how we import these modules and the components within them to our programs. So here we are importing csv, a module Python provides to handle CSV files.

Then we are defining a list of dictionaries, that is the information we want to store in our CSV. With the with statement, we are calling a built-in function called open, which receives the location of a file, a mode to open it and a new line character (among other stuff we don’t need right now), then it returns the file it opened which is used as context in our with statement. The mode “w+” we used to open our file, means that we want to create the file if it doesn’t exist and write in it.

The trickiest part comes now, we are using a method provided by dictionaries that is called keys and returns an iterable containing all the keys in our dictionary and then sorting it with the built-in function sorted, as we know keys in a dictionary are not always ordered the same way, and it could be very chaotic if every time we write in a csv the columns are sorted differently. The rest of the writing process is just how you use the csv module.

On the read process there is another weird thing going on with a for statement between square brackets. That is called for comprehension, we are basically building an array of d for every d in an iterable (csv.DictReader(csv_file) returns an iterable with every row in our csv file).

The last for is just printing out the dicts in read_dictionaries. Let’s check out the output:

$ python3 flow-control/with.py
{'value': '123', 'id': '0'}
{'value': '749', 'id': '1'}
{'value': '913', 'id': '2'}

Now, if you’ve ever worked with files in other languages, you remember how you had to close the resources manually after reading/writing from a file. The with statement is doing it for us, as files are one of many resources supported by this statement. There are resources not supported by this feature, like sockets, but there is always a workaround.

7. Functions

This gets more and more interesting. Functions are a big deal in Python, as they are first-class objects. This means that we can do a lot of fun stuff with them here. Let’s first check out the syntax:

def function_name(arguments):
    body

Pretty simple, huh? The def keyword always starts the definition of a function, then comes its name, arguments between parentheses, a colon and then the body. A return statement is optional.

basic.py

def format_hello(name):
    return "hello " + name + "!"

print(format_hello("Sebastián"))

Here we defined a function called format_hello that takes one argument called name and returns a custom hello message.

Let’s go back to that “functions are first-class objects” thing. What can we do with functions in Python? Well, we can treat them as variables, so we can assign them to other variables, we can pass them around as arguments, we can print them, we can define functions inside another function and we can return functions from other functions. Let’s see some of this features at work.

first_class.py

def a_function(an_array, a_function_argument):
    return [a_function_argument(item) for item in an_array]


def add_one(n):
    return n + 1

my_array = [5, 6, 7, 8]

print(str(a_function(my_array, add_one)))

Here we are defining a function that receives an array and another function as arguments and applies the argument function to each element of the array, returning the resulting array using for comprehension. Then we defined a function add_one that receives a number as argument and returns that number plus one. Let’s check out the output:

$ python3 functions/first_class.py
[6, 7, 8, 9]

There it is. The behavior we expected. That’s an awesome feature, right? We are passing a function as argument, this might not seem a big deal for you, but it is a very useful feature. We’ll get back to it later, now let’s see what other things can be done with functions.

One thing I struggle a lot with other languages, like Java or JavaScript, is to set default values to function parameters. You have to check if the argument is present and if it’s not use the default value… I hate it. Python, as many other languages, provides an easy way of setting default values to function parameters. Check this out.

default_parameter.py

def add(a, b=1):
    return a + b

print(str(add(2, 2)))
print(str(add(9)))

Here we defined a function called add that receives two arguments, a and b, and returns de sum of them, but b as a default value (1). So when we call this function to print the results we can provide both arguments, or simply provide a and the default b is used. See the output below.

$ python3 functions/default_parameter.py
4
10

See? 2 + 2 = 4 and 9 + 1 = 10. I love this feature, but it has a big gotcha. The default values are not created when the function is called, but when it is defined instead. That means that, every time you use the default value of a parameter, it will be the exact same instance. Why I am telling you this? Well, because if you use a type that is mutable as a default value, you might get surprised about the results. See:

mutable_default_parameter.py

def append_to(item, array=[]):
    array.append(item)
    return array

print(append_to(1))
print(append_to(2))
print(append_to(2, []))
print(append_to(3))

This piece of code seems harmless, you might expect 4 different arrays to be printed out, [1], [2], [2] again and [3], but this won’t happen, as you are modifying the default value of the parameter, so the output actually is:

$ python3 functions/mutable_default_parameter.py
[1]
[1, 2]
[2]
[1, 2, 3]

When we provide an array as argument (third print statement), a new array is created, but otherwise not. The same array is being modified again and again. Having this in mind, you shouldn’t avoid to have mutable default arguments in your functions, but know how to treat them instead. See, this would be a correct implementation of the above method:

mutable_default_parameter.py

def append_to(item, array=[]):
    return array + [item]

print(append_to(1))
print(append_to(2))
print(append_to(2, []))
print(append_to(3))

The append_to function is not modifying the arguments. This function is compliant with functional programming, it doesn’t modify the given array, but return a new one instead. The output is now what we wanted it to be:

$ python3 functions/mutable_default_parameter.py
[1]
[2]
[2]
[3]

I feel a better person just by telling you this, I’m sure that if you remember this you’ll do fine with Python in production environments.

With this knowledge we can jump into another very useful feature Python has for us. Decorators.

Python decorators provide a nice and simple syntax to call higher-order functions. By definition, a decorator takes a function as a parameter, and returns a wrapper of that given function to extend its behavior without actually modifying it.

Imagine you want to print the arguments and results of a couple functions. There are two ways of doing this:

  • Inserting print statements in all the functions in which you want this behavior
  • Python decorators

Let’s solve this problem with decorators so I show you how they work:

decorators.py

def print_args_and_result(target):
    def wrapper(*args):
        print("calling {} with arguments: {}".format(target.__name__, str(args)))
        result = target(*args)
        print("{} result: {}".format(target.__name__, str(result)))
        return result
    return wrapper


@print_args_and_result
def power(a, b):
    return a ** b


@print_args_and_result
def root(a, b):
    return a ** (1 / b)


print(power(5, 2))
print(root(9, 2))

We defined a function called print_args_and_result that receives a function as an argument and returns a wrapper, this by definition is a decorator. The star symbol (*) before the arguments tell Python that the number of arguments we receive can vary, it’s like saying “this function can receive any number of arguments”. This way, we receive the arguments as a tuple args. You’ll see it when we see the output.

The {} in string is a place holder, so we can call the function format the str type provides, that will replace the place holders with the arguments provided. Also, functions, classes, and other objects in Python has some attributes that are defined by the language, one of them is __name__ that holds, in this case, the function’s name.

Now the decorator is defined, we annotate with it all the functions we want to decorate, and it’s just magic. Python will override our functions with the wrapper the decorator returns. The output below.

$ python3 functions/decorators.py
calling power with arguments: (5, 2)
power result: 25
25
calling root with arguments: (9, 2)
root result: 3.0
3.0

Now, as I said at the top of the tutorial, Python is an object-oriented language, so we’ve got classes around here.

8. Classes

A class is a user-defined prototype for an object that defines a set of attributes that characterize any object of the class. The attributes are data members (class variables and instance variables) and methods, accessed via dot notation.

To create a class we use the keyword class as in:

class MyClass:
    'optional documentation, like java docs'
    class stuff

Here we defined a class called MyClass. The documentation can be accessed via the class attribute __doc__. Let’s talk about that class stuff:

  • Classes should have a constructor, which is a special function Python will call when an instance of a class is created.
  • There are Class Variables, which are variables available for all instances of a class.
  • Instance variables are variables that belongs only to the current instance of a class.
  • There are functions and methods in classes, they have only one difference from normal functions, and we’ll talk about it after the next example.

Here is an example of a, kind of, complete class:

definition.py

class Dog:

    """A Dog representation"""
    animal = "dog"

    def __init__(self, name, breed):
        self.name = name
        self.breed = breed
        self.age = 0

    def birth_day(self):
        self.age += 1

    def get_age(self):
        return self.age

    def get_name(self):
        return self.name

if __name__ == '__main__':
    qwerty = Dog("Qwerty", "German Shepperd")
    while qwerty.get_age() < 9:
        qwerty.birth_day()
    print("My {}, {}, has {} years".format(qwerty.animal, qwerty.get_name(), qwerty.get_age()))

Here we are defining a class called Dog, which has a string documentation defined with triple double-quotes. Strings defined with triple double-quotes can contain enters, it just look nicer in the code.

This class has a class variable called animal which holds the value "dog", this variable will be available with the same value for all instances of this class.

The constructor method of every class is called __init__, and, as every other method in a class, the first parameter is self, self is a reference to the instance in question. It’s kind of messy if you come from other languages, but it is not that hard actually, you don’t need to worry about this argument as Python injects it implicitly every time you call a function of a class’ instance.

The self argument can be seen as the this in Java or JavaScript, it’s just that, it isn’t just a global keyword, but an argument present in every function. You’ll get the hang of it by practicing.

On the constructor method of this class we are receiving the name and the breed of the dog, and assigning those values to instance variables, along with the age initialized in zero.

Then there is a method called birth_day that adds one to the age of the dog. The other two methods are accessor methods. This is kind of weird because every variable in an object (whether they are instance variables or class variables) are public, but as I come from JVM languages I just like to have accessor methods and leave the variables of an object alone.

The if __name__ == '__main__': clause is the correct way of defining the top-level script that must be executed only if the script is being called as such. In other words, I want this code to be executed only if this code is being called with python3 |file_name|, but if this script is being imported by another file, this code should not be executed. Let’s check the output:

$ python3 classes/definition.py
My dog, Qwerty, has 9 years

There it is, we defined a class and actually added some behavior to it. You see how we created an instance of that class? Just by running Dog("qwerty", "german shepperd") we are creating an instance of Dog, which is calling the __init__ method, as we can see by the fact that the name and age are being properly initialized.

To invoke functions within classes we just use dot notation, as in Class.function(args). And just to clarify the self argument issue, as you can see, we are not providing it explicitly, Python takes care of it for us. A thing to notice about it, is that when we call birth_day to access the instance variable age, we need to do it through self, which also happens in all the other accessors.

Now let’s talk about inheritance. Instead of creating two classes that look the same but one has an attribute that another one does not, you can create a class that has every attribute and function that those classes have in common, and make those sub-classes of the parent class you’ve just created. Let’s see this in practice by creating a Cat class in the above example.

definition.py

class Cat:

    """A Cat representation"""
    animal = "cat"

    def __init__(self, name, breed):
        self.name = name
        self.breed = breed
        self.age = 0

    def birth_day(self):
        self.age += 1

    def get_age(self):
        return self.age

    def get_name(self):
        return self.name


class Dog:

    """A Dog representation"""
    animal = "dog"

    def __init__(self, name, breed):
        self.name = name
        self.breed = breed
        self.age = 0

    def birth_day(self):
        self.age += 1

    def get_age(self):
        return self.age

    def get_name(self):
        return self.name

if __name__ == '__main__':
    qwerty = Dog("Qwerty", "German Shepperd")
    raul = Cat("Raul", "Siamese")
    while qwerty.get_age() < 9:
        qwerty.birth_day()
    while raul.get_age() < 3:
        raul.birth_day()
    print("My {}, {}, has {} years".format(qwerty.animal, qwerty.get_name(), qwerty.get_age()))
    print("My {}, {}, has {} years".format(raul.animal, raul.get_name(), raul.get_age()))

See? These classes look the same, there is a lot of repeated code here. Well, let’s make a parent class called Animal and make Dog and Cat subclasses of Animal. The code looks like this:

definition.py

class Animal:

    """An animal representation"""

    def __init__(self, animal_type, name, breed):
        self.animal_type = animal_type
        self.name = name
        self.breed = breed
        self.age = 0

    def birth_day(self):
        self.age += 1

    def get_age(self):
        return self.age

    def get_name(self):
        return self.name


class Cat(Animal):

    """A Cat representation"""

    def __init__(self, name, breed):
        Animal.__init__(self, "cat", name, breed)


class Dog(Animal):

    """A Dog representation"""

    def __init__(self, name, breed):
        Animal.__init__(self, "dog", name, breed)

if __name__ == '__main__':
    qwerty = Dog("Qwerty", "German Shepperd")
    raul = Cat("Raul", "Siamese")
    while qwerty.get_age() < 9:
        qwerty.birth_day()
    while raul.get_age() < 3:
        raul.birth_day()
    print("My {}, {}, has {} years".format(qwerty.animal_type, qwerty.get_name(), qwerty.get_age()))
    print("My {}, {}, has {} years".format(raul.animal_type, raul.get_name(), raul.get_age()))

This code looks nicer, more elegant and scalable. If we wan’t to add a Cow to our system, now takes four lines of executable code, and that is only if we add the documentation.

If you read through the whole article, you now know a lot about Python. It would be rude not to show you how you can build an application with all this knowledge right?

9. A Little Practice

Let’s build a software for one of those places where they help lost dogs and cats to find new owners. Our application will be called PuppyPlace.

First of all, we need a model. We’ll write a file model.py where we will define our business model.

model.py

class Animal:

    """An animal representation in PuppyPlace system"""

    def __init__(self, id, date, breed, estimated_age, notes=[]):
        self.id = id
        self.admission_date = date
        self.breed = breed
        self.estimated_age = estimated_age
        for note in notes:
            self.validate_note_type(note)
        self.notes = notes.copy()

    def get_id(self):
        return self.id

    def get_admission_date(self):
        return self.admission_date

    def get_breed(self):
        return self.breed

    def get_estimated_age(self):
        return self.estimated_age

    def get_notes(self):
        return self.notes

    def validate_note_type(self, note):
        if type(note) == str:
            return True
        else:
            raise TypeError("A note should be a string message")

    def add_note(self, note):
        if self.validate_note_type(note):
            self.notes = self.notes + [note]


class Cat(Animal):

    """A cat representation in PuppyPlace system"""

    def __init__(self, id, date, breed, estimated_age, notes=[]):
        Animal.__init__(self, id, date, breed, estimated_age, notes)


class Dog(Animal):

    """A dog representation in PuppyPlace system"""

    def __init__(self, id, date, breed, estimated_age, notes=[]):
        Animal.__init__(self, id, date, breed, estimated_age, notes)

Notice that, in the Animal constructor, the default value for the notes argument is a list, which is mutable. I use the function copy that iterables provide, this function returns a shallow copy of the iterable instance. Also, we are validating that notes must be string objects.

We’ve got our model properly defined, now we need a place to store the animals. Let’s write a file called repository.py where we’ll define in-memory repositories to store these entities.

repository.py

class Repository:

    """In memory storage for PuppyPlace entities"""

    def __init__(self):
        self.storage = {}

    def create_or_update(self, id, entry):
        self.storage[id] = entry

    def delete(self, id):
        del self.storage[id]

    def find_by_id(self, id):
        return self.storage.get(id)

    def all(self):
        return list(self.storage.values())

Now, we glue it all together in a file called service.py where we’ll build our entities and store them in our repository.

service.py

import repository
import model
from datetime import date


class CatService:

    def __init__(self):
        self.repository = repository.Repository()
        self.next_id = 0

    def store(self, breed, estimated_age, notes=[]):
        cat = model.Cat(self.next_id, date.today(), breed, estimated_age, notes)
        self.repository.create_or_update(cat.get_id(), cat)
        self.next_id += 1

    def update(self, cat):
        self.repository.create_or_update(cat.get_id(), cat)

    def remove(self, id):
        self.repository.delete(id)

    def get_by_id(self, id):
        return self.repository.find_by_id(id)

    def all(self):
        return self.repository.all()


class DogService:

    def __init__(self):
        self.repository = repository.Repository()
        self.next_id = 0

    def store(self, breed, estimated_age, notes=[]):
        dog = model.Dog(self.next_id, date.today(), breed, estimated_age, notes)
        self.repository.create_or_update(dog.get_id(), dog)
        self.next_id += 1

    def update(self, dog):
        self.repository.create_or_update(dog.get_id(), dog)

    def remove(self, id):
        self.repository.delete(id)

    def get_by_id(self, id):
        return self.repository.find_by_id(id)

    def all(self):
        return self.repository.all()

These services receive some parameters and build our entities to store them in our repositories. Here we encapsulated some logic that we will not have to worry about later. There is something new here, in the imports. The from keyword. Sometimes, there are big modules that contain a lot of classes and functions, and maybe we only want to import one class to use it. Well, we can do that with the from keyword.

These services shall do the job. Now let’s jump to the user interface. It will be a console application, so we’ll write a menu and receive some input from the user, and then display the information through print statements. The way of getting input from the user in a console application with Python, is with the built-in function input, which receives a prompt message as argument and returns the string the user provided. So, let’s write a file called menu.py and start PuppyPlace’s UI.

menu.py

from service import DogService, CatService


class Menu:
    def __init__(self, options={}):
        self.options = options.copy()
        self.options[0] = "Exit"

    def print_options(self):
        keys = sorted(self.options.keys())
        for k in keys:
            print("{}. {}".format(k, self.options.get(k)))
        try:
            selected = int(input("What do you want to do? "))
        except ValueError:
            print("Please provide a valid option.")
            selected = None
        return selected


class AnimalSelectionMenu(Menu):
    def __init__(self, dog_callback, cat_callback):
        Menu.__init__(self, {
            1: "Dog",
            2: "Cat"
        })
        self.dog_callback = dog_callback
        self.cat_callback = cat_callback

    def start(self):
        selected = self.print_options()
        if selected == 1:
            self.dog_callback()
        elif selected == 2:
            self.cat_callback()


class GuestCheckInMenu:
    def __init__(self, dog_service, cat_service):
        self.dog_service = dog_service
        self.cat_service = cat_service

    def create_dog(self):
        self.dog_service.store(input("breed: "), input("estimated age: "))

    def create_cat(self):
        self.cat_service.store(input("breed: "), input("estimated age: "))

    def start(self):
        AnimalSelectionMenu(self.create_dog, self.create_cat).start()


class GuestCheckOutMenu:
    def __init__(self, dog_service, cat_service):
        self.dog_service = dog_service
        self.cat_service = cat_service

    def check_out_dog(self):
        ListMenu(self.dog_service, self.cat_service).list_dogs()
        id = int(input("dog id: "))
        self.dog_service.remove(id)

    def check_out_cat(self):
        ListMenu(self.dog_service, self.cat_service).list_cats()
        id = int(input("cat id: "))
        self.cat_service.remove(id)

    def start(self):
        AnimalSelectionMenu(self.check_out_dog, self.check_out_cat).start()


class ListMenu:
    def __init__(self, dog_service, cat_service):
        self.dog_service = dog_service
        self.cat_service = cat_service

    def list_dogs(self):
        sorted_dogs = sorted(self.dog_service.all(), key=lambda d: d.get_id())
        dogs = map(lambda dog: "{}) {} - {} - {} - {}".format(dog.get_id(), dog.get_admission_date(), dog.get_breed(),
                                                              dog.get_estimated_age(), ",".join(dog.get_notes())),
                   sorted_dogs)
        for dog in dogs:
            print(dog)

    def list_cats(self):
        sorted_cats = sorted(self.cat_service.all(), key=lambda c: c.get_id())
        cats = map(lambda cat: "{}) {} - {} - {} - {}".format(cat.get_id(), cat.get_admission_date(), cat.get_breed(),
                                                              cat.get_estimated_age(), ",".join(cat.get_notes())),
                   sorted_cats)
        for cat in cats:
            print(cat)

    def start(self):
        AnimalSelectionMenu(self.list_dogs, self.list_cats).start()


class AddNotesMenu:
    def __init__(self, dog_service, cat_service):
        self.dog_service = dog_service
        self.cat_service = cat_service

    def add_notes_dog(self):
        ListMenu(self.dog_service, self.cat_service).list_dogs()
        id = int(input("dog id: "))
        note = input("note: ")
        self.dog_service.get_by_id(id).add_note(note)

    def add_notes_cat(self):
        ListMenu(self.dog_service, self.cat_service).list_cats()
        id = int(input("cat id: "))
        note = input("note: ")
        self.cat_service.get_by_id(id).add_note(note)

    def start(self):
        AnimalSelectionMenu(self.add_notes_dog, self.add_notes_cat).start()


class MainMenu(Menu):
    def __init__(self, dog_service=DogService(), cat_service=CatService()):
        Menu.__init__(self, {
            1: "Guest Check-In",
            2: "Guest Check-Out",
            3: "Guests List",
            4: "Add Notes"
        })
        self.dog_service = dog_service
        self.cat_service = cat_service

    def check_in(self):
        GuestCheckInMenu(self.dog_service, self.cat_service).start()

    def check_out(self):
        GuestCheckOutMenu(self.dog_service, self.cat_service).start()

    def list(self):
        ListMenu(self.dog_service, self.cat_service).start()

    def add_notes(self):
        AddNotesMenu(self.dog_service, self.cat_service).start()

    def start(self):
        print("Welcome to Puppy Place's system!")

        selected = None
        while selected != 0:

            selected = self.print_options()

            if selected == 1:
                self.check_in()
            elif selected == 2:
                self.check_out()
            elif selected == 3:
                self.list()
            elif selected == 4:
                self.add_notes()

Here it is, this is our user interface. This file is kind of big, if this was a real application we should split this classes into smaller modules.

There are a couple new things here again, lambda expressions, a new parameter in the built-in function sorted and a new built-in function map. Let’s start with the lambda expressions.

Lambdas are functions defined on the fly, they are convenient when we have to pass functions as arguments, as sometimes we don’t want to define those functions in a scope where other pieces of code have access to. Also, sometimes those functions have no other use, and we just need them as argument to a map or sorted.

The syntax is really simple, it’s just defined as lambda parameter1, parameter2, ..., parameterN: body. A return clause is not needed, as lambdas always have to return something and are one-liners (functions with only one line in their body), so the return statement is implicit there.

The key argument in the sorted built-in function is a function that receives an element of the array we are sorting, and returns the value the algorithm must use as sorting criteria. In this case, the key argument is a lambda that receives one of our entities and returns its id, as we want the animals sorted by id.

The map function is one of the most used built-in functions for iterables. It receives a function (called transformation) and an array, then returns an array with the results of applying the transformation to every element of the provided array. You can see the map function more in detail in this example.

So, now we need a main script, which we’ll write in a file called main.py.

menu.py

from menu import MainMenu

if __name__ == '__main__':
    MainMenu().start()

This script only starts the MainMenu. Now, let’s test this!

python3 main.py
Welcome to Puppy Place's system!
0. Exit
1. Guest Check-In
2. Guest Check-Out
3. Guests List
4. Add Notes
What do you want to do? 1
0. Exit
1. Dog
2. Cat
What do you want to do? 1
breed: German Shepperd
estimated age: 4
0. Exit
1. Guest Check-In
2. Guest Check-Out
3. Guests List
4. Add Notes
What do you want to do? 1
0. Exit
1. Dog
2. Cat
What do you want to do? 1
breed: Poodle
estimated age: 8
0. Exit
1. Guest Check-In
2. Guest Check-Out
3. Guests List
4. Add Notes
What do you want to do? 1
0. Exit
1. Dog
2. Cat
What do you want to do? 2
breed: Siamese
estimated age: 9
0. Exit
1. Guest Check-In
2. Guest Check-Out
3. Guests List
4. Add Notes
What do you want to do? 3
0. Exit
1. Dog
2. Cat
What do you want to do? 1
0) 2016-03-14 - German Shepperd - 4 -
1) 2016-03-14 - Poodle - 8 -
0. Exit
1. Guest Check-In
2. Guest Check-Out
3. Guests List
4. Add Notes
What do you want to do? 2
0. Exit
1. Dog
2. Cat
What do you want to do? 1
0) 2016-03-14 - German Shepperd - 4 -
1) 2016-03-14 - Poodle - 8 -
dog id: 0
0. Exit
1. Guest Check-In
2. Guest Check-Out
3. Guests List
4. Add Notes
What do you want to do? 1
0. Exit
1. Dog
2. Cat
What do you want to do? 1
breed: Schnauzer
estimated age: 5
0. Exit
1. Guest Check-In
2. Guest Check-Out
3. Guests List
4. Add Notes
What do you want to do? 3
0. Exit
1. Dog
2. Cat
What do you want to do? 1
1) 2016-03-14 - Poodle - 8 -
2) 2016-03-14 - Schnauzer - 5 -
0. Exit
1. Guest Check-In
2. Guest Check-Out
3. Guests List
4. Add Notes
What do you want to do? 3
0. Exit
1. Dog
2. Cat
What do you want to do? 2
0) 2016-03-14 - Siamese - 9 -
0. Exit
1. Guest Check-In
2. Guest Check-Out
3. Guests List
4. Add Notes
What do you want to do? 4
0. Exit
1. Dog
2. Cat
What do you want to do? 2
0) 2016-03-14 - Siamese - 9 -
cat id: 0
note: Violent
0. Exit
1. Guest Check-In
2. Guest Check-Out
3. Guests List
4. Add Notes
What do you want to do? 3
0. Exit
1. Dog
2. Cat
What do you want to do? 2
0) 2016-03-14 - Siamese - 9 - Violent
0. Exit
1. Guest Check-In
2. Guest Check-Out
3. Guests List
4. Add Notes
What do you want to do? 0

See? It works! Download the project and test it yourself. Also, there is homework for you! See the code of the services and menus of cats and dogs? They all look the same! Why don’t you try and improve that?

10. Download the Code Project

This was an extensive introduction to Python. Keep in mind that there is still a lot to learn from this awesome language!

Download
You can download the full source code of this example here: python-beginners

Sebastian Vinci

Sebastian is a full stack programmer, who has strong experience in Java and Scala enterprise web applications. He is currently studying Computers Science in UBA (University of Buenos Aires) and working a full time job at a .com company as a Semi-Senior developer, involving architectural design, implementation and monitoring. He also worked in automating processes (such as data base backups, building, deploying and monitoring applications).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button