X Tutup
Skip to content

Latest commit

 

History

History
2186 lines (1829 loc) · 122 KB

File metadata and controls

2186 lines (1829 loc) · 122 KB

Index

1. [[#Intro to Programming]]
2. [[#Python is]]
3. [[#Best practices]]
4. [[#Python Built-In Data Types]]
5. [[#Python Keywords]]
6. [[#Operators by priority]]
8. [[#Functions]]
9. [[#Scopes]]
10. [[#Methods]]
11. [[#Lists]]
12. [[#Error Handling]]
13. [[#Error Types]]
14. [[#Exceptions]]
15. [[#Cool things to keep in mind]]
16. [[#How to read Python]]
17. [[#Examples]]

Intro to Programming

A program makes a computer usable. Without a program, a computer, even the most powerful one, is nothing more than an object. Similarly, without a player, a piano is nothing more than a wooden box.

Computers are able to perform very complex tasks, but this ability is not innate. A computer's nature is quite different.

It can execute only extremely simple operations. For example, a computer cannot understand the value of a complicated mathematical function by itself, although this isn't beyond the realms of possibility in the near future.

Contemporary computers can only evaluate the results of very fundamental operations, like adding or dividing, but they can do it very fast, and can repeat these actions virtually any number of times.

A computer, even the most technically sophisticated, is devoid of even a trace of intelligence. You could say that it is like a well-trained dog - it responds only to a predetermined set of known commands.

Computers are Binary

The name 'boolean' comes from George Boole (1815-1864), the author of the fundamental work, The Laws of Thought, which contains the definition of Boolean algebra ‒ a part of algebra which makes use of only two distinct values: True and False, denoted as 1 and 0.

A programmer writes a program, and the program asks questions. A computer executes the program, and provides the answers. The program must be able to react according to the received answers.

Fortunately, computers know only two kinds of answers:

  • Yes, this is true;
  • No, this is false.

You'll never get a response like: I don't know or Probably yes, but I don't know for sure.

False is when all the bits are reset; True is when at least one bit is set.

Actions that form a Program

Imagine that you want to know the average speed you've reached during a long journey. You know the distance, you know the time, you need the speed.

Naturally, the computer will be able to compute this, but the computer is not aware of such things as distance, speed, or time. Therefore, it is necessary to instruct the computer to:

  1. Input
    • accept a number representing the distance;
    • accept a number representing the travel time;
  2. Processing
    • divide the former value by the latter and store the result in the memory;
  3. Output
    • display the result (representing the average speed) in a readable format.

These four simple actions form a program. Of course, these examples are not formalized, and they are very far from what the computer can understand, but they are good enough to be translated into a language the computer can accept.

Language is the keyword.

Language elements

While we humans communicate through natural languages, computers communicate through machine language, which is very rudimentary, both of which consist of the following:

  1. Alphabet
  2. Lexis
  3. Syntax
  4. Semantics
Computer Programming

Computer programming is the act of composing the selected programming language's elements in the order that will cause the desired effect. The effect could be different in every specific case – it's up to the programmer's imagination, knowledge and experience.

Of course, such a composition has to be correct in many senses:

  • alphabetically – a program needs to be written in a recognizable script, such as Roman, Cyrillic, etc.
  • lexically – each programming language has its dictionary and you need to master it; thankfully, it's much simpler and smaller than the dictionary of any natural language;
  • syntactically – each language has its rules and they must be obeyed;
  • semantically – the program has to make sense.

Unfortunately, a programmer can also make mistakes with each of the above four senses. Each of them can cause the program to become completely useless.

Let's assume that you've successfully written a program. How do we persuade the computer to execute it? You have to render your program into machine language. Luckily, the translation can be done by a computer itself, making the whole process fast and efficient.

Compilation vs Interpretation

There are two different ways of transforming a program from a high-level programming language into machine language:

  1. COMPILATION
  • Doesn't need compiler to run (after being compiled)
  • The execution is usually faster, but the code can't be easily edited
  • May take time to fully compile
  • Source code is hidden from the user and can't be shared easily as it needs to be compiled for each kind of architecture
  1. INTERPRETATION
  • Needs interpreter to run
  • Usually slower than compiled because the computer resources are shared among the source program and the interpreter itself
  • Is executed instantly, line by line
  • Source code is easily shareable and works anywhere there is a interpreter
What does the interpreter do?

Let's assume once more that you have written a program. Now, it exists as a computer file: a computer program is actually a piece of text, so the source code is usually placed in text files.

Note: it has to be pure text, without any decorations like different fonts, colors, embedded images or other media. Now you have to invoke the interpreter and let it read your source file.

The interpreter reads the source code in a way that is common in Western culture: from top to bottom and from left to right. There are some exceptions - they'll be covered later in the course.

First of all, the interpreter checks if all subsequent lines are correct (using the four aspects covered earlier).

If the compiler finds an error, it finishes its work immediately. The only result in this case is an error message.

The interpreter will inform you where the error is located and what caused it. However, these messages may be misleading, as the interpreter isn't able to follow your exact intentions, and may detect errors at some distance from their real causes.

For example, if you try to use an entity of an unknown name, it will cause an error, but the error will be discovered in the place where it tries to use the entity, not where the new entity's name was introduced.

In other words, the actual reason is usually located a little earlier in the code, for example, in the place where you had to inform the interpreter that you were going to use the entity of the name.

If the line looks good, the interpreter tries to execute it (note: each line is usually executed separately, so the trio "read-check-execute" can be repeated many times - more times than the actual number of lines in the source file, as some parts of the code may be executed more than once).

It is also possible that a significant part of the code may be executed successfully before the interpreter finds an error. This is normal behavior in this execution model.

You may ask now: which is better? The "compiling" model or the "interpreting" model? There is no obvious answer. If there had been, one of these models would have ceased to exist a long time ago. Both of them have their advantages and their disadvantages.


Python is

  • a high-level interpreted programming language with dynamic semantics
  • an interpreted programming language
  • object-oriented
  • used for general-purpose programming
  • easy and intuitive, understandable, suitable for everyday tasks
  • easy to learn, teach, use, understand, obtain

Best practices

  • Functions: The name of the function should be significant (the name of the print function is self-evident).
  • Comments: Comments are very important. They are used not only to make your programs easier to understand, but also to disable those pieces of code that are currently not needed (e.g., when you need to test some parts of your code only, and ignore others).
  • Variable names: Whenever possible and justified, you should give self-commenting names to variables, e.g., if you're using two variables to store the length and width of something, the variable names length and width may be a better choice than myvar1 and myvar2.
  • It's good to describe each important piece of code with comments, use self-commenting and readable (non-confusing) variable names, and sometimes it's better to divide your code into named pieces (e.g., functions). In some situations, it's a good idea to write the steps of computations in a clearer way.
  • Don't feel obliged to code your programs in a way that is always the shortest and the most compact. Readability may be a more important factor. Keep your code ready for a new programmer.
  • Using the same name of a built-in Python function as a variable name would generally be considered bad practice. Ex.: using sum considering there already is sum()
  • if a particular fragment of the code begins to appear in more than one place, consider the possibility of isolating it in the form of a function invoked from the points where the original code was placed before.
  • if a piece of code becomes so large that reading and understating it may cause a problem, consider dividing it into separate, smaller problems, and implement each of them in the form of a separate function
  • if you're going to divide the work among multiple programmers, decompose the problem to allow the product to be implemented as a set of separately written functions packed together in different modules.

Python Built-In Data Types

  1. None Type: None is a NoneType object, and it is used to represent the absence of a value. There are only two kinds of circumstances when None can be safely used:
    • when you assign it to a variable (or return it as a function's result)
    • when you compare it with a variable to diagnose its internal state.
  2. Numeric Types: all support arithmetic; represent numerical quantities; NOT containers; IMMUTABLE
    • Integers (int): whole numbers. Ex.: 5
    • Floats (float): numbers with decimals. Ex.: 3.14
    • Complex (complex): numbers that store real + imaginary. Ex.: 2+3j
  3. Boolean Type (bool): equals True or False
  4. Text Type:
    • Strings (str): represents human text; supports slicing, indexing; IMMUTABLE, cannot do math, behaves like a character sequence
  5. Sequence (Ordered container) Types: store multiple values; keep order; allow indexing and iteration; NOT sets
    • Lists list: resizable array of values; MUTABLE (i.e. references a place in memory which if altered through another variable also alters this because these just reference the same value in memory)
    • Tuple tuple: fixed array of values; IMMUTABLE
    • Range range: efficient sequence of integers; IMMUTABLE
  6. Set Types: do NOT keep order (UNORDERED); do NOT allow duplicates; support mathematical set operations
    • Set (set): MUTABLE
    • Frozen Set (frozenset): IMMUTABLE
  7. Mapping Type: stores key → value pairs, not just values; fast lookup by key; internal structure is a hash table
    • Dictionary (dict): MUTABLE
  8. Binary Types: represent raw bytes, not characters, not numbers. Used for networking, cryptography, file manipulation, image/audio processing
    • bytes: raw binary data; IMMUTABLE
    • bytearray: modifiable binary data; MUTABLE
    • memoryview: view into another binary buffer; IMMUTABLE
  9. Callable / Class / Function Types: Python treats functions, methods, and classes as data (you can store them, move them around, and use them as data)
    • Functions: can be stored in variables, passed as arguments
    • Classes: blueprint for objects
    • Methods: function bound to an object
  10. Special / Internal Types: exist mostly for advanced use
Type Example Purpose
type type(x) metaclass of all classes
object root of everything
ellipsis ... used in slicing (rare)
NotImplementedType NotImplemented used in operator overloading

Python Keywords

Known
['False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

False, True  # boolean values, 0 and 1
None  # Null equivalent in python, it means the absence of value
if, elif, else  # conditions for decisions
pass  # empty instruction to bypass syntax demands of if, elif, else, while, for, etc
while, for, break, continue, else  # loops
and, or, not  # logical operators
del  # instruction used to delete a list element, slices and/or the list itself
in, not in  # check whether a specific value is stored inside a list or not
def, return, None  # functions, return is a instruction
is  # comparison with None
global  # used to allow assigning values to variables outside of a function's scope
try, except, finally, raise  # validation
import, from, as  # working with modules and entities

with, as  # working with files
Need to learn

'assert', 'class', '', 'lambda', 'nonlocal', 'yield'


Operators by priority

  • Basic: **; unary +, unary -; *, /, //, %; binary +, binary -

  • Shift: << and >> (pair of digraphs)

  • Comparison/Relational: > (greater than), >= (greater than or equal to), < (less than), <= (less than or equal to); == (equal to), != (not equal to)

  • Logical: not (negation); and (conjunction); or (disjunction)

  • Assignment: =

  • Shortcut: +=, -=, *=, /=, //=, %=, **=

  • Bitwise (abbreviated form): ~ (negation); & (conjunction); ^ (XOR); | (disjunction)

[!NOTE]- More about bitwise operations

  • Bitwise: allow you to manipulate single bits of data. The arguments of these operators must be integers. The difference in the operation of the logical and bit operators is important: the logical operators do not penetrate into the bit level of its argument. They're only interested in the final integer value. Bitwise operators are stricter: they deal with every bit separately.
    • & (ampersand) ‒ bitwise conjunction;
    • | (bar) ‒ bitwise disjunction;
    • ~ (tilde) ‒ bitwise negation;
    • ^ (caret) ‒ bitwise exclusive or (xor).

Bitwise operations (&, |, and ^)

Argument A Argument B A & B A | B A ^ B
0 0 0 0 0
0 1 0 1 1
1 0 0 1 1
1 1 1 1 0

Bitwise operations (~)

Argument ~ Argument
0 1
1 0

Let's make it easier:

  • & requires exactly two 1s to provide 1 as the result;
  • | requires at least one 1 to provide 1 as the result;
  • ^ requires exactly one 1 to provide 1 as the result.

Examples:

i = 15
j = 22

### Conjunction:
log = i and j # log == True

bit = i & j # bit == 6
# because
#   i: 00000000000000000000000000001111
#   j: 00000000000000000000000000010110
# bit: 00000000000000000000000000000110

### Negation:
logneg = not i # logneg == False

bitneg = ~i # bitneg == -16
# because
#      i: 00000000000000000000000000001111
# bitneg: 11111111111111111111111111110000
# If we interpret that as an **unsigned** 8-bit number, it would be **240**.
# But Python integers are **signed** (they can be negative), and it uses
# **two’s complement** logic to handle signs conceptually.
# So to find its decimal value:
# 1. Invert the bits
#	 → `00000000000000000000000000001111`
# 2. Add 1  
#    → `00000000000000000000000000010000` = 16
# 3. Then put the negative sign:
#    → `-16`

You can use bitwise operators to manipulate single bits of data. The following sample data:

  • x = 15, which is 0000 1111 in binary,
  • y = 16, which is 0001 0000 in binary.

will be used to illustrate the meaning of bitwise operators in Python. Analyze the examples below:

  • & does a bitwise and, e.g., x & y = 0, which is 0000 0000 in binary,
  • | does a bitwise or, e.g., x | y = 31, which is 0001 1111 in binary,
  • ˜ does a bitwise not, e.g., ˜ x = 240*, which is 1111 0000 in binary,
  • ^ does a bitwise xor, e.g., x ^ y = 31, which is 0001 1111 in binary,
  • >> does a bitwise right shift, e.g., y >> 1 = 8, which is 0000 1000 in binary,
  • << does a bitwise left shift, e.g., y << 3 = 128, which is 1000 0000 in binary.

* -16 (decimal from signed 2's complement) -- read more about the Two's complement operation.

Binary left shift and binary right shift

Python offers yet another operation relating to single bits: shifting. This is applied only to integer values, and you mustn't use floats as arguments for it.

You already apply this operation very often and quite unconsciously. How do you multiply any number by ten? Take a look:

12345 × 10 = 123450

As you can see, multiplying by ten is in fact a shift of all the digits to the left and filling the resulting gap with zero.

Division by ten? Take a look:

12340 ÷ 10 = 1234

Dividing by ten is nothing but shifting the digits to the right.

The same kind of operation is performed by the computer, but with one difference: as two is the base for binary numbers (not 10), shifting a value one bit to the left thus corresponds to multiplying it by two; respectively, shifting one bit to the right is like dividing by two (notice that the rightmost bit is lost).

The shift operators in Python are a pair of digraphs<< and >>, clearly suggesting in which direction the shift will act.

value << bits
value >> bits
 

The left argument of these operators is an integer value whose bits are shifted. The right argument determines the size of the shift.

It shows that this operation is certainly not commutative.

The priority of these operators is very high. You'll see them in the updated table of priorities, which we'll show you at the end of this section.

Take a look at the shifts in the editor window.

var = 17
var_right = var >> 1
var_left = var << 2
print(var, var_left, var_right)

The final print() invocation produces the following output:

17 68 8

Note:

  • 17 >> 1 → 17 // 2 (17 floor-divided by 2 to the power of 1) → 8 (shifting to the right by one bit is the same as integer division by two)
  • 17 << 2 → 17 * 4 (17 multiplied by 2 to the power of 2) → 68 (shifting to the left by two bits is the same as integer multiplication by four)
Priority Operator Notes Type
1 ~, +, - unary tilde (Bitwise Negation), unary plus/minus (Arithmetic)
2 ** exponentiation (Arithmetic)
3 *, /, //, % multiplication, division, floor/integer division, modulo (Arithmetic)
4 +, - binary binary plus/minus (Arithmetic)
5 <<, >> pair of digraphs (Bitwise Shift)
6 <, <=, >, >= less than, less than or equal to, greater than, greater than or equal to (Comparison)
7 ==, != equality, inequality (Comparison)
8 & ampersand (Bitwise Conjunction)
9 ^ caret (Bitwise Exclusive or)
10 | bar (Bitwise Disjunction)
11 =, +=, -=, *=, /=, %=, &=, ^=, |=, >>=, <<= Assignment Operator + Shortcuts/Abbreviated Forms
Priority Operators Description
1 +, - (unary) Unary plus/minus (e.g., -x, +y)
2 ** Exponentiation
3 *, /, //, % Multiplication, division, floor division, modulo
4 +, - (binary) Binary addition and subtraction
5 <, <=, >, >= Comparison operators
6 ==, != Equality and inequality

Conditional Execution/Instruction/Statement

There's at least two possibilities on how to make use of the boolean answers the computer gives to comparison questions:

  1. Memorize it (store it in a variable) and make use of it later
  2. Use the answer you get to make a decision about the future of the program

Functions

[!NOTE]-

Why do we need functions?

You've come across functions many times so far, but the view on their merits that we have given you has been rather one-sided. You've only invoked functions by using them as tools to make life easier, and to simplify time-consuming and tedious tasks.

When you want some data to be printed on the console, you use print(). When you want to read the value of a variable, you use input(), coupled with either int() or float().

You've also made use of some methods, which are in fact functions, but declared in a very specific way.

Now you'll learn how to write and use your own functions. We'll write several functions together, from the very simple to the rather complex, which will require your focus and attention.

It often happens that a particular piece of code is repeated many times in your program. It's repeated either literally, or with only a few minor modifications, consisting of the use of other variables in the same algorithm. It also happens that a programmer cannot resist simplifying their work, and begins to clone such pieces of code using the clipboard and copy-paste operations.

It could end up as greatly frustrating when suddenly it turns out that there was an error in the cloned code. The programmer will have a lot of drudgery to find all the places that need corrections. There's also a high risk of the corrections causing errors.

We can now define the first condition which can help you decide when to start writing your own functions: if a particular fragment of the code begins to appear in more than one place, consider the possibility of isolating it in the form of a function invoked from the points where the original code was placed before.

It may happen that the algorithm you're going to implement is so complex that your code begins to grow in an uncontrolled manner, and suddenly you notice that you're not able to navigate through it so easily anymore.

You can try to cope with the issue by commenting the code extensively, but soon you find that this dramatically worsens your situation ‒ too many comments make the code larger and harder to read. Some say that a well-written function should be viewed entirely in one glance.

A good, attentive developer divides the code (or more accurately: the problem) into well-isolated pieces, and encodes each of them in the form of a function.

This considerably simplifies the work of the program, because each piece of code can be encoded separately, and tested separately. The process described here is often called decomposition.

We can now state the second condition: if a piece of code becomes so large that reading and understating it may cause a problem, consider dividing it into separate, smaller problems, and implement each of them in the form of a separate function.

This decomposition continues until you get a set of short functions, easy to understand and test.

Decomposition

It often happens that the problem is so large and complex that it cannot be assigned to a single developer, and a team of developers have to work on it. The problem must be split between several developers in a way that ensures their efficient and seamless cooperation.

It seems inconceivable that more than one programmer should write the same piece of code at the same time, so the job has to be dispersed among all the team members.

This kind of decomposition has a different purpose to the one described previously ‒ it's not only about sharing the work, but also about sharing the responsibility among many developers.

Each of them writes a clearly defined and described set of functions, which when combined into the module (we'll tell you about this a bit later) will give the final product.

This leads us directly to the third condition: if you're going to divide the work among multiple programmers, decompose the problem to allow the product to be implemented as a set of separately written functions packed together in different modules.

  • A function is a block of code that performs a specific task when the function is called (invoked). You can use functions to make your code reusable, better organized, and more readable. Functions can have parameters and return values.
  • Functions may have none or many (positional/keyword) parameters meaning they take none or many arguments and either:
    1. cause some effect (e.g., send text to the terminal, create a file, draw an image, play a sound, etc.); this is something completely unheard of in the world of mathematics. Ex.: print()
    2. evaluate a value (e.g., the square root of a value or the length of a given text) and return it as the function's result; this is what makes Python functions the relatives of mathematical concepts. Ex.: sqrt()
  • Functions come from Python itself (built-in), from pre-installed modules, from your code and the lambdafunctions.
  • You mustn't invoke a function which is not known at the moment of invocation.
  • You mustn't have a function and a variable of the same name.
  • You can pass information to functions by using parameters. Your functions can have as many parameters as you need.
    • The function's full power reveals itself when it can be equipped with an interface that is able to accept data provided by the invoker. Such data can modify the function's behavior, making it more flexible and adaptable to changing conditions.
    • A parameter is actually a variable, but there are two important factors that make parameters different and special:
      • parameters exist only inside functions in which they have been defined, and the only place where the parameter can be defined is a space between a pair of parentheses in the def statement;
      • assigning a value to the parameter is done at the time of the function's invocation, by specifying the corresponding argument.
    • Don't forget:
      • parameters live inside functions (this is their natural environment)
      • arguments exist outside functions, and are carriers of values passed to corresponding parameters.
      • There is a clear and unambiguous frontier between these two worlds.
    • A value for the parameter will arrive from the function's environment.
    • Remember: specifying one or more parameters in a function's definition is also a requirement, and you have to fulfil it during invocation. You must provide as many arguments as there are defined parameters. Failure to do so will cause an error.
  • You can pass arguments to a function using the following techniques:
    • positional argument passing in which the order of arguments passed matters (Ex. 1)
    • keyword (named) argument passing in which the order of arguments passed doesn't matter (Ex. 2)
    • a mix of positional and keyword argument passing (Ex. 3.)
Ex. 1
def subtra(a, b):
    print(a - b)

subtra(5, 2)    # outputs: 3
subtra(2, 5)    # outputs: -3


Ex. 2
def subtra(a, b):
    print(a - b)

subtra(a=5, b=2)    # outputs: 3
subtra(b=2, a=5)    # outputs: 3

Ex. 3
def subtra(a, b):
    print(a - b)

subtra(5, b=2)    # outputs: 3
subtra(5, 2)    # outputs: 3
  • It's important to remember that positional arguments mustn't follow keyword arguments. That's why if you try to run the following snippet:
def subtra(a, b):
    print(a - b)

subtra(5, b=2)    # outputs: 3
subtra(a=5, 2)    # Syntax Error

Python will not let you do it by signalling a SyntaxError.

  • You can use the keyword argument-passing technique to pre-define a value for a given argument:
def name(first_name, last_name="Smith"):
    print(first_name, last_name)

name("Andy")    # outputs: Andy Smith
name("Betty", "Johnson")    # outputs: Betty Johnson (the keyword argument replaced by "Johnson")
  • The return instruction has two different variants:
    • returnwithout an expression causes a function's termination
    • return with an expression causes the immediate termination of the function's execution (just like before), moreover, the function will evaluate the expression's value and will return it (hence the name once again) as the function's result.
  • The return instruction, enriched with the expression (the expression is very simple here), "transports" the expression's value to the place where the function has been invoked.
    • Don't forget:
      • you are always allowed to ignore the function's result, and be satisfied with the function's effect (if the function has any)
      • if a function is intended to return a useful result, it must contain the second variant of the return instruction.
  • Don't forget this: if a function doesn't return a certain value using a return expression clause, it is assumed that it implicitly returns None.
Known Functions

print(), round(), input(), int(), float(), str(), max(), min(), range() (float), time.sleep(), len() (lists)

Important things to know about functions:
  • print() can work with octal and hex values: print(0o10) equals 8 while print(0x10) equals 16
    • Note: oct() and hex() can be used for type casting
  • round(): signature round(number[, ndigits]), numbers less than .5 round down, numbers greater than .5 round up, but numbers exactly at .5 are an exception: Python uses Round Half to Even rounding.

[!EXAMPLE]- Demonstration

>>> x = 10.501
>>> round(x, -1) # nearest multiple of 10
10.0
>>> round(x) # nearest integer
11
>>> round(x, 0) # same but with the floating point
11.0
>>> round(x, 1) # 1 decimal place
10.5
>>> round(x, 2) # 2 decimal places
10.5
>>> round(x, 3) # 3 decimal places
10.501
>>> round(x, 4) # 4 decimal places
10.501

>>> x = -10.501
>>> round(x, -1)
-10.0
>>> round(x, 0)
-11.0
>>> round(x, 1)
-10.5
>>> round(x, 2)
-10.5
>>> round(x, 3)
-10.501
>>> round(x, 4)
-10.501

[!NOTE]- Round Half to Even (Banker's Rounding, IEEE 754 Standard)

.5 values round toward the nearest even integer, not always up, to keep results statistically unbiased across large datasets.

Value Nearest Integers Rounded To Reason
-12.5 [-12] , -13 -12 -12 is even
-11.5 -11 , [-12] -12 -12 is even
-10.5 [-10] , -11 -10 -10 is even
-9.5 -9 , [-10] -10 -10 is even
-8.5 [-8] , -9 -8 -8 is even
-7.5 -7 , [-8] -8 -8 is even
-6.5 [-6] , -7 -6 -6 is even
-5.5 -5 , [-6] -6 -6 is even
-4.5 [-4] , -5 -4 -4 is even
-3.5 -3 , [-4] -4 -4 is even
-2.5 [-2] , -3 -2 -2 is even
-1.5 -1 , [-2] -2 -2 is even
-0.5 [0] , -1 0 0 is even
0.5 [0] , 1 0 0 is even
1.5 1 , [2] 2 2 is even
2.5 [2] , 3 2 2 is even
3.5 3 , [4] 4 4 is even
4.5 [4] , 5 4 4 is even
5.5 5 , [6] 6 6 is even
6.5 [6] , 7 6 6 is even
7.5 7 , [8] 8 8 is even
8.5 [8] , 9 8 8 is even
9.5 9 , [10] 10 10 is even
10.5 [10] , 11 10 10 is even
11.5 11 , [12] 12 12 is even
12.5 [12] , 13 12 12 is even
  • range() (range(start, stop[, step])) works both ways: range(1, 5, 1) equals 1234 while range(1, 5, -1) equals 5432

Scopes

  • The scope of a name (e.g., a variable name) is the part of a code where the name is properly recognizable. For example, the scope of a function's parameter is the function itself. The parameter is inaccessible outside the function.
  • A variable existing outside a function has scope inside the function's body, excluding those which define a variable of the same name. It also means that the scope of a variable existing outside a function is supported only when getting its value (reading). Assigning a value forces the creation of the function's own variable.
  • There's a special Python method which can extend a variable's scope in a way which includes the function's body (even if you want not only to read the values, but also to modify them): global. You can use the global keyword followed by a variable name to make the variable's scope global
    • Using this keyword inside a function with the name (or names separated with commas) of a variable (or variables), forces Python to refrain from creating a new variable inside the function ‒ the one accessible from outside will be used instead. In other words, this name becomes global (it has global scope, and it doesn't matter whether it's the subject of read or assign).
def fun(x):
    global y
    y = x * x
    return y


fun(2)
print(y)  # Output: 4
  • changing the parameter's value doesn't propagate outside the function (in any case, not when the variable is a scalar, like in the example). This also means that a function receives the argument's value, not the argument itself. This is true for scalars.
  • if the argument is a list, then changing the value of the corresponding parameter doesn't affect the list (remember: variables containing lists are stored in a different way than scalars), but if you change a list identified by the parameter (note: the list, not the parameter!), the list will reflect the change.

Methods

  • method is a specific kind of function ‒ it behaves like a function and looks like a function, but differs in the way in which it acts, and in its invocation style.
  • function doesn't belong to any data ‒ it gets data, it may create new data and it (generally) produces a result. A method does all these things, but is also able to change the state of a selected entity.
  • A method is owned by the data it works for, while a function is owned by the whole code. This also means that invoking a method requires some specification of the data from which the method is invoked.

It may sound puzzling here, but we'll deal with it in depth when we delve into object-oriented programming.

In general, a typical function invocation may look like this:

result = function(arg) 

The function takes an argument, does something, and returns a result.

A typical method invocation usually looks like this:

result = data.method(arg) 

Note: the name of the method is preceded by the name of the data which owns the method. Next, you add a dot, followed by the method name, and a pair of parenthesis enclosing the arguments.

The method will behave like a function, but can do something more ‒ it can change the internal state of the data from which it has been invoked.

This is an essential issue right now, as we're going to show you how to add new elements to an existing list. This can be done with methods owned by all the lists, not by functions.

Known Methods:
time.sleep() # `sleep()` is a **function** defined in the `time` module, and when you call it as `time.sleep()`, it behaves like a method of that module.
list.append(value) # It takes its argument's value and puts it **at the end of the list** which owns the method.
list.insert(location, value) # it can add a new element **at any place in the list**, not only at the end
list.sort() # to timsort in ascending order (default)
list.reverse() # to reverse a list order by swapping the first element with the last and so on

Lists

Why do we need lists? It may happen that you have to read, store, process, and finally, print dozens, maybe hundreds, perhaps even thousands of numbers. To make this process simpler, instead of needing to create and use multiple scalar variables (able to store exactly one given value at a time), you may use a list (variable that can store more than one value) to store multiple values.

The list is a type of data in Python used to store multiple objects. It is an ordered and mutable collection of comma-separated items between square brackets, e.g.: my_list = [1, None, True, "I am a string", 256, 0]. Lists can be nested, e.g.: my_list = [1, 'a', ["list", 64, [0, 1], False]]

Indexing

In order to facilitate the selection of any list element, Python has adopted a convention stating that the elements in a list are always numbered starting from zero. This means that the item stored at the beginning of the list will have the number zero (index).

The value inside the brackets which selects one element of the list is called an index, while the operation of selecting/retrieving or modifying an element from the list by index is known as indexing.

An element with an index equal to -1 is the last one in the list. Similarly, the element with an index equal to -2 is the one before last in the list.

Operations by index
numbers = [10, 5, 7, 2, 1]
print("Original list contents:", numbers)  # Output: [10, 5, 7, 2, 1]

numbers[0] = 111  # Accessing the list's first element and replacing its value
print("\nPrevious list contents:", numbers)  # Output: [111, 5, 7, 2, 1]

numbers[1] = numbers[4]  # Copying value of the fifth element to the second.
print("New list contents:", numbers)  # Output: [111, 1, 7, 2, 1]

print("\nList's length:", len(numbers))  # Printing previous list length. Output: 5

del numbers[1]  # Removing the second element from the list.
print("New list's length:", len(numbers))  # Printing new list length. Output: 4
print("\nNew list content:", numbers)  # Output: [111, 7, 2, 1]

print(numbers[-1])  # Output: 1
print(numbers[-2])  # Output: 2

del numbers  # Deletes the whole list

###
my_list = [10, 1, 8, 3, 5]
total = 0

for i in range(len(my_list)):
    total += my_list[i]

print(total)  # Output: 27
Operations without index
# By iteration:
my_list = [10, 1, 8, 3, 5]
total = 0

for i in my_list:
    total += i

print(total)  # Output: 27
Operations by methods

A method can change the internal state of the data from which it has been invoked.

  • list.append(value): takes its argument's value and puts it at the end of the list which owns the method.
  • list.insert(location, value): adds a new element at any place in the list, not only at the end. It takes two arguments:
    • the first shows the required location of the element to be inserted; note: all the existing elements that occupy locations to the right of the new element (including the one at the indicated position) are shifted to the right, in order to make space for the new element;
    • the second is the element to be inserted.
  • list.reverse(): reverses the list

[!EXAMPLE]- More about .reverse() Considering my_list = [10, 1, 8, 3, 5], my_list.reverse() does the same as:

my_list = [10, 1, 8, 3, 5]

my_list[0], my_list[4] = my_list[4], my_list[0]
my_list[1], my_list[3] = my_list[3], my_list[1]

print(my_list)  # Output: [5, 3, 8, 1, 10]

or better yet:

my_list = [10, 1, 8, 3, 5]
length = len(my_list)

for i in range(length // 2):
    my_list[i], my_list[length - i - 1] = my_list[length - i - 1], my_list[i]

print(my_list)  # Output: [5, 3, 8, 1, 10]
  • list.sort(): sorts the elements of a list

[!EXAMPLE]- More about .sort() Considering my_list = [8, 10, 6, 2, 4], my_list.sort() does better than the following because it uses Timsort instead of Bubble sort:

my_list = [8, 10, 6, 2, 4] # list to sort swapped = True # It's a little fake, we need it to enter the while loop.

while swapped: swapped = False # no swaps so far for i in range(len(my_list) - 1): if my_list[i] > my_list[i + 1]: swapped = True # a swap occurred! my_list[i], my_list[i + 1] = my_list[i + 1], my_list[i]

print(my_list) # Output: [2, 4, 6, 8, 10]

or
```python
my_list = []

swapped = True num = int(input("How many elements do you want to sort: "))

for i in range(num): val = float(input("Enter a list element: ")) my_list.append(val)

while swapped: swapped = False for i in range(len(my_list) - 1): if my_list[i] > my_list[i + 1]: swapped = True my_list[i], my_list[i + 1] = my_list[i + 1], my_list[i]

print("\nSorted:") print(my_list)

numbers = [111, 7, 2, 1]
print(f"Length: {len(numbers)} | List: {numbers}")

numbers.append(4)
print(f"Length: {len(numbers)} | List: {numbers}")

numbers.insert(0, 222)  # method + indexing
print(f"Length: {len(numbers)} | List: {numbers}")

numbers.insert(1, 333)  # method + indexing
print(f"Length: {len(numbers)} | List: {numbers}")

lst = [5, 3, 1, 2, 4]
lst.sort()
print(lst)  # outputs: [1, 2, 3, 4, 5]

lst = ["D", "F", "A", "Z"]
lst.sort()
print(lst)  # Output: ['A', 'D', 'F', 'Z']

lst = [5, 3, 1, 2, 4]
lst.reverse()
print(lst)  # outputs: [4, 2, 1, 3, 5]

[!WARNING]- IMPORTANT - A list content is referenced not named While the name of an ordinary variable is the name of its content, the name of a list is the name of a memory location where the list is stored. Therefore, assigning a list to another list then modifying the latter will also alter the former as both identify the same location in computer memory.

list_1 = [1]
list_2 = list_1
list_1[0] = 2
print(list_2)  # Output: [2]

Use slicing instead.

Slicing

A slice is an element of Python syntax that allows you to make a brand new copy of a list, or parts of a list. It actually copies the list's contents, not the list's name.

Syntax: my_list[start:end]

  • start is the index of the first element included in the slice. If you omit the start in your slice, it is assumed that you want to get a slice beginning at the element with index 0.
  • end is the index of the first element not included in the slice. Similarly, if you omit the end in your slice, it is assumed that you want the slice to end at the element with the index len(my_list).
  • A slice of this form makes a new (target) list, taking elements from the source list ‒ the elements of the indices from start to end - 1.
    • Note: not to end but to end - 1. An element with an index equal to end is the first element which does not take part in the slicing.
  • Using negative values for both start and end is possible (just like in indexing).
    • Note: If the start specifies an element lying further than the one described by the end (from the list's beginning), the slice will be empty.
Slicing Operations

Considering: my_list = [10, 8, 6, 4, 2]

  • my_list[start:] means my_list[start:len(my_list)] so, new_list = my_list[3:] equals [4, 2]
  • my_list[:end] means my_list[0:end] so, new_list = my_list[:3] equals [10, 8, 6]
  • my_list[:] selects an entire list: [10, 8, 6, 4, 2]
  • my_list[1:3] the more precise way of slicing: [8, 6]
    • Starts at element 2 (index 1);
    • Ends with element 3 (index 2) right before element 4 (index 3)
  • my_list[1:-1] selects anything from index 1 until the last list element (-1): [8, 6, 4]
  • my_list[-1:1] equals []
  • del my_list[1:3] deletes slices, so [10, 4, 2]
  • del my_list[:] deletes all elements, so []
  • del my_list deletes the list itself not its content, so if new_list = my_list before deletion, new_listequals [10, 8, 6, 4, 2]
List comprehensions
  • A special syntax used by Python in order to fill massive lists which allows you to create new lists from existing ones in a concise and elegant way.
  • The syntax of a list comprehension looks as follows: [expression for element in list if conditional] which is actually an equivalent of the following code:
for element in list:
    if conditional:
        expression 
  • It is actually a list but created on-the-fly during program execution, and is not described statically.

Example #1: squares = [x ** 2 for x in range(10)] 

The snippet produces a ten-element list filled with squares of ten integer numbers starting from zero (0, 1, 4, 9, 16, 25, 36, 49, 64, 81)

Example #2: twos = [2 ** i for i in range(8)] 

The snippet creates an eight-element array containing the first eight powers of two (1, 2, 4, 8, 16, 32, 64, 128)

Example #3: odds = [x for x in squares if x % 2 != 0 ] 

The snippet makes a list with only the odd elements of the squares list.

Two-dimensional arrays

Let's also assume that a predefined symbol named EMPTY designates an empty field on the chessboard.

So, if we want to create a list of lists representing the whole chessboard, it may be done in the following way:

board = [] for i in range(8):     row = [EMPTY for i in range(8)]     board.append(row) 

Note:

  • the inner part of the loop creates a row consisting of eight elements (each of them equal to EMPTY) and appends it to the board list;
  • the outer part repeats it eight times;
  • in total, the board list consists of 64 elements (all equal to EMPTY)

This model perfectly mimics the real chessboard, which is in fact an eight-element list of elements, all being single rows. Let's summarize our observations:

  • the elements of the rows are fields, eight of them per row;
  • the elements of the chessboard are rows, eight of them per chessboard.

The board variable is now a two-dimensional array. It's also called, by analogy to algebraic terms, a matrix.

As list comprehensions can be nested, we can shorten the board creation in the following way:

board = [[EMPTY for i in range(8)] for j in range(8)] 

The inner part creates a row, and the outer part builds a list of rows.

Access to the selected field of the board requires two indices ‒ the first selects the row; the second ‒ the field number inside the row, which is de facto a column number.

Multidimensional nature of lists: advanced applications

Let's go deeper into the multidimensional nature of lists. To find any element of a two-dimensional list, you have to use two coordinates:

  • a vertical one (row number)
  • and a horizontal one (column number).

Imagine that you're developing a piece of software for an automatic weather station. The device records the air temperature on an hourly basis and does it throughout the month. This gives you a total of 24 × 31 = 744 values. Let's try to design a list capable of storing all these results.

First, you have to decide which data type would be adequate for this application. In this case, a float would be best, since this thermometer is able to measure the temperature with an accuracy of 0.1 ℃.

Then you take an arbitrary decision that the rows will record the readings every hour on the hour (so the row will have 24 elements) and each of the rows will be assigned to one day of the month (let's assume that each month has 31 days, so you need 31 rows). Here's the appropriate pair of comprehensions (h is for hour, d for day):

temps = [[0.0 for h in range(24)] for d in range(31)]

The whole matrix is filled with zeros now. You can assume that it's updated automatically using special hardware agents. The thing you have to do is to wait for the matrix to be filled with measurements.


Now it's time to determine the monthly average noon temperature. Add up all 31 readings recorded at noon and divide the sum by 31. You can assume that the midnight temperature is stored first. Here's the relevant code:

temps = [[0.0 for h in range(24)] for d in range(31)]
#
# The matrix is magically updated here.
#

total = 0.0

for day in temps:
    total += day[11]

average = total / 31

print("Average temperature at noon:", average)

Note: the day variable used by the for loop is not a scalar ‒ each pass through the temps matrix assigns it with the subsequent rows of the matrix; hence, it's a list. It has to be indexed with 11 to access the temperature value measured at noon.


Now find the highest temperature during the whole month ‒ see the code:

temps = [[0.0 for h in range(24)] for d in range(31)]
#
# The matrix is magically updated here.
#

highest = -100.0

for day in temps:
    for temp in day:
        if temp > highest:
            highest = temp

print("The highest temperature was:", highest)

Note:

  • the day variable iterates through all the rows in the temps matrix;
  • the temp variable iterates through all the measurements taken in one day.

Now count the days when the temperature at noon was at least 20 ℃:

temps = [[0.0 for h in range(24)] for d in range(31)]
#
# The matrix is magically updated here.
#

hot_days = 0

for day in temps:
    if day[11] > 20.0:
        hot_days += 1

print(hot_days, "days were hot.")

Python does not limit the depth of list-in-list inclusion. Here you can see an example of a three-dimensional array:

rooms = [[[False for r in range(20)] for f in range(15)] for t in range(3)]

Imagine a hotel. It's a huge hotel consisting of three buildings, 15 floors each. There are 20 rooms on each floor. For this, you need an array which can collect and process information on the occupied/free rooms.

First step ‒ the type of the array's elements. In this case, a Boolean value (True/False) would fit.

Step two ‒ calm analysis of the situation. Summarize the available information: three buildings, 15 floors, 20 rooms.

Now you can create the array:

rooms = [[[False for r in range(20)] for f in range(15)] for t in range(3)]

The first index (0 through 2) selects one of the buildings; the second (0 through 14) selects the floor, the third (0 through 19) selects the room number. All rooms are initially free.

Now you can book a room for two newlyweds: in the second building, on the tenth floor, room 14:

rooms[1][9][13] = True

and release the second room on the fifth floor located in the first building:

rooms[0][4][1] = False

Check if there are any vacancies on the 15th floor of the third building:

vacancy = 0

for room_number in range(20):
    if not rooms[2][14][room_number]:
        vacancy += 1

The vacancy variable contains 0 if all the rooms are occupied, or the number of available rooms otherwise.

Key properties:

  • denoted by square brackets: [] with its elements separated by commas ,
  • multi-value: Able to store more than one value
  • dynamic size: You can grow or shrink it automatically
  • heterogeneous: Allows different types inside (e.g.: numbers, strings, objects, even other lists, etc)
  • ordered: Items are stored in sequence and have indexes
  • mutable: You can modify it by interacting with any variable that points to it in memory

Error Handling

  • Dealing with programming errors has (at least) two sides:

    • The one appears when you get into trouble because your – apparently correct – code is fed with bad data i.e. exceptions, which occur even when a statement/expression is syntactically correct; these are the errors that are detected during execution when your code results in an error which is not uncoditionally fatal.
    • The other side of dealing with programming errors reveals itself when undesirable code behavior is caused by mistakes you made when you were writing your program (a.k.a. a bug) i.e. syntax errors (parsing errors), which occur when the parser comes across a statement that is incorrect.
  • To protect your code from termination, the user from disappointment, and yourself from the user's dissatisfaction, you must check and test your code to treat exceptions.

  • In Python we don't do preliminary data validation but instead: "It’s better to beg for forgiveness than to ask for permission" which means "it's better to handle an error when it happens than to try to avoid it"

  • Use the try-except branch to catch exceptions to gracefully handle them: try marks the place where you try to do something without permission

    • this is the place where you put the code you suspect is risky and may be terminated in case of error; note: this kind of error is called an exception, while the exception occurrence is called raising – we can say that an exception is (or was) raised
    • any part of the code placed between try and except is executed in a very special way – any error which occurs here won't terminate program execution. Instead, the control will immediately jump to the first line situated after the except keyword, and no other part of the try branch is executed.

    except starts a location where you can show off your apology talents

    • the part of the code starting with the except keyword is designed to handle the exception; it's up to you what you want to do here: you can clean up the mess or you can just sweep the problem under the carpet (although we would prefer the first solution);
    • the code in the except branch is activated only when an exception has been encountered inside the try block. There is no way to get there by any other means.
  • when either the try block or the except block is executed successfully, the control returns to the normal path of execution, and any code located beyond in the source file is executed as if nothing happened.

  • You can "catch" and handle exceptions in Python by using the try-except block. So, if you have a suspicion that any particular snippet may raise an exception, you can write the code that will gracefully handle it, and will not interrupt the program.

  • You can use multiple except blocks within one try statement, and specify particular exception names. If one of the except branches is executed, the other branches will be skipped. Remember: you can specify a particular built-in exception only once. Also, don't forget that the default (or generic) exception, that is the one with no name specified, should be placed at the bottom of the branch (use the more specific exceptions first, and the more general last).

  • Some of the most useful Python built-in exceptions are: ZeroDivisionErrorValueErrorTypeErrorAttributeError, and SyntaxError. One more exception that, in our opinion, deserves your attention is the KeyboardInterrupt exception, which is raised when the user hits the interrupt key (CTRL-C or Delete). Run the code above and hit the key combination to see what happens. To learn more about the Python built-in exceptions, consult the official Python documentation.

  • Last but not least, you should remember about testing and debugging your code. Use such debugging techniques as print debugging; if possible – ask someone to read your code and help you to find bugs in it or to improve it; try to isolate the fragment of code that is problematic and susceptible to errors: test your functions by applying predictable argument values, and try to handle the situations when someone enters wrong values; comment out the parts of the code that obscure the issue. Finally, take breaks and come back to your code after some time with a fresh pair of eyes.

Dealing with more than one exception

Now we want to ask you an innocent question: is ValueError the only way the control could fall into the except branch?

The answer is obviously "no" – there is more than one possible way to raise an exception. For example, a user may enter zero as an input – can you predict what will happen next?

Yes, you're right – the division placed inside the print() function invocation will raise the ZeroDivisionError. As you may expect, the code's behavior will be the same as in the previous case – the user will see the "I do not know what to do..." message, which seems to be quite reasonable in this context, but it's also possible that you would want to handle this kind of problem in a bit different way.

Is it possible? Of course, it is. There are at least two approaches you can implement here.

The first of them is simple and complicated at the same time: you can just add two separate try blocks, one including the input() function invocation where the ValueError may be raised, and the second devoted to handling possible issues induced by the division. Both these try blocks would have their own except branches, and in effect you will gain full control over two different errors.

This solution is good, but it is a bit lengthy – the code becomes unnecessarily bloated. Moreover, it's not the only danger that awaits you. Note that leaving the first try-except block leaves a lot of uncertainty – you will have to add extra code to ensure that the value the user has entered is safe to use in division. This is how a seemingly simple solution becomes overly complicated.

Fortunately, Python offers a simpler way to deal with this kind of challenge.

Two exceptions after one try

Look at the code in the editor. As you can see, we've just introduced the second except branch. This is not the only difference – note that both branches have exception names specified. In this variant, each of the expected exceptions has its own way of handling the error, but it must be emphasized that only one of all branches can intercept the control – if one of the branches is executed, all the other branches remain idle.

  • The number of except branches is not limited – you can specify as many or as few of them as you need, but don't forget that none of the exceptions can be specified more than once.
  • We've added a third except branch, but this time it has no exception name specified – we can say it's anonymous or (what is closer to its actual role) it's the default. You can expect that when an exception is raised and there is no except branch dedicated to this exception, it will be handled by the default branch.
  • The default except branch must be the last except branch. Always!
Why you can't avoid testing your code

Although we're going to wrap up our exceptional considerations here, don't think it's all Python can offer to help you with begging for forgiveness. Python's exception machinery is far more complex, and its capabilities allow you to build expanded error handling strategies. We'll return to these issues – we promise. Feel free to conduct your experiments and to dive into exceptions yourself.

An important duty for developers is to test the newly created code, but you must not forget that testing isn't a way to prove that the code is error-free. Paradoxically, the only proof testing can provide is that your code contains errors. Don’t think you can relax after a successful test.

Now we want to tell you about the second side of the never-ending struggle with errors – the inevitable destiny of a developer's life. As you are not able to avoid making bugs in your code, you must always be ready to seek out and destroy them. Don't bury your head in the sand – ignoring errors won't make them disappear.

The second important aspect of software testing is strictly psychological. It's a truth known for years that authors – even those who are reliable and self-aware – aren't able to objectively evaluate and verify their works.

This is why each novelist needs an editor and each programmer needs a tester. Some say – a little spitefully but truthfully – that developers test the code to show their perfection, not to find problems that may frustrate them. Testers are free of such dilemmas, and this is why their work is more effective and profitable.

Of course, this doesn't absolve you from being attentive and careful. Test your code as best you can. Don't make the testers' work too easy.

Your primary duty is to ensure that you’ve checked all execution paths your code can go through. Does that sound mysterious? Nothing of the kind!

Tests, testing, and testers

The answer is simpler than you may expect, and a bit disappointing, too. Python – as you know for sure – is an interpreted language. This means that the source code is parsed and executed at the same time. Consequently, Python may not have time to analyze the code lines which aren't subject to execution. As an old developer's saying states: "it's a feature, not a bug" (please don't use this phrase to justify your code's weird behavior).

Do you understand now why passing through all execution paths is so vital and inevitable?

Let’s assume that you complete your code and the tests you've made are successful. You deliver your code to the testers and – fortunately! – they found some bugs in it. We’re using the word "fortunately" completely consciously. You need to accept that, firstly, testers are the developer’s best friends – don't treat the bugs they discover as an offense or a malignancy; and, secondly, each bug the testers find is a bug that won't affect the users. Both factors are valuable and worth your attention.

You already know that your code contains a bug or bugs (the latter is more likely). How do you locate them and how do you fix your code?

Bug vs. debug

The basic measure a developer can use against bugs is – unsurprisingly – a debugger, while the process during which bugs are removed from the code is called debugging. According to an old joke, debugging is a complicated mystery game in which you are simultaneously the murderer, the detective, and – the most painful part of the intrigue – the victim. Are you ready to play all these roles? Then you must arm yourself with a debugger.

A debugger is a specialized piece of software that can control how your program is executed. Using the debugger, you can execute your code line-by-line, inspect all the variables' states and change their values on demand without modifying the source code, stop program execution when certain conditions are or aren't met, and do lots of other useful tasks.

We can say that every IDE is equipped with a more or less advanced debugger. Even IDLE has one, although you may find its handling a bit complicated and troublesome. If you want to make use of IDLE's integrated debugger, you should activate it using the “Debug” entry in the main IDLE window menu bar. It's the start point for all debugger facilities.

Click here to see the screenshots that show the IDLE debugger during a simple debugging session. (Thank you, University of Kentucky!)

You can see how the debugger visualizes variables and parameter values, and note the call stack which shows the chain of invocations leading from the currently executed function to the interpreter level.

If you want to know more about the IDLE debugger, consult the IDLE documentation.

print debugging

This form of debugging, which can be applied to your code using any kind of debugger, is sometimes called interactive debugging. The meaning of the term is self-explanatory – the process needs your (the developer's) interaction to be performed.

Some other debugging techniques can be used to hunt bugs. It's possible that you aren't able or don't want to use a debugger (the reasons may vary). Are you helpless then? Absolutely not!

You may use one of the simplest and the oldest (but still useful) debugging tactics known as print debugging. The name speaks for itself – you just insert several additional print() invocations inside your code to output data which illustrates the path your code is currently negotiating. You can output the values of the variables which may affect the execution.

These printouts may output meaningful text like "I am here""I entered the foo() function""The result is 0", or they may contain sequences of characters that are legible only to you. Please don't use obscene or indecent words for the purpose, even though you may feel a strong temptation – your reputation can be ruined in a moment if these antics leak to the public.

As you can see, this kind of debugging isn't really interactive at all, or is interactive only to a small extent, when you decide to apply the input() function to stop or delay code execution.

After the bugs are found and removed, the additional printouts may be commented out or removed – it's up to you. Don't let them be executed in the final code – they may confuse both testers and users.

Some useful tips

Here are some tips which may help you to find and eliminate the bugs. None of them is either ultimate or definitive. Use them flexibly and rely on your intuition. Don't believe yourself – check everything twice.

  1. Try to tell someone (for example, your friend or coworker) what your code is expected to do and how it actually behaves. Be concrete and don't omit details. Answer all questions your helper asks. You'll likely realize the cause of the problem while telling your story, as speaking activates these parts of your brain which remain idle during coding. If no human can help you with the problem, use a yellow rubber duck instead. We're not kidding – consult the Wikipedia article to learn more about this commonly used technique: Rubber Duck Debugging.
  2. Try to isolate the problem. You can extract the part of your code that is suspected of being responsible for your troubles and run it separately. You can comment out parts of the code that obscure the problem. Assign concrete values to variables instead of reading them from the input. Test your functions by applying predictable argument values. Analyze the code carefully. Read it aloud.
  3. If the bug has appeared recently and didn't show up earlier, analyze all the changes you've introduced into your code – one of them may be the reason.
  4. Take a break, drink a cup of coffee, take your dog and go for a walk, read a good book for a moment or two, make a phone call to your best friend – you'll be surprised how often it helps.
  5. Be optimistic – you'll find the bug eventually; we promise you this.
Unit testing – a higher level of coding

There is also one important and widely used programming technique that you will have to adopt sooner or later during your developer career – it's called unit testing. The name may a bit confusing, as it's not only about testing the software, but also (and most of all) about how the code is written.

To make a long story short – unit testing assumes that tests are inseparable parts of the code and preparing the test data is an inseparable part of coding. This means that when you write a function or a set of cooperating functions, you're also obliged to create a set of data for which your code's behavior is predictable and known.

Moreover, you should equip your code with an interface that can be used by an automated testing environment. In this approach, any amendment made to the code (even the least significant) should be followed by the execution of all the unit tests accompanied by your source.

To standardize this approach and make it easier to apply, Python provides a dedicated module named unittest. We're not going to discuss it here – it's a broad and complex topic.

Therefore, we’ve prepared a separate course and certification path for this subject. It is called “Testing Essentials with Python”, and we invite you to participate in it.


Error Types

Let’s discuss in more detail some useful (or rather, the most common) exceptions you may experience.

  • AttributeError happens when you try to activate a method which doesn't exist in an item you're dealing with i.e. when you try to use a property or method that doesn’t exist for an object. Example: number = 42 then number.append(5)
  • IndexError happens when you try to access a position in a list (or similar structure) that doesn’t exist. Example: my_list = [10, 20, 30] then print(my_list[5])
  • ModuleNotFoundError
  • NameError happens when you try to use a variable or function name that hasn’t been defined. Example: print(score)when scorehasn't been defined yet.
  • RecursionError
  • SyntaxError raised when the control reaches a line of code which violates Python's grammar. It may sound strange, but some errors of this kind cannot be identified without first running the code. This kind of behavior is typical of interpreted languages – the interpreter always works in a hurry and has no time to scan the whole source code. It is content with checking the code which is currently being run. It's a bad idea to handle this exception in your programs. You should produce code that is free of syntax errors, instead of masking the faults you’ve caused.
  • TypeError happens when you try to apply a data whose type cannot be accepted in the current context i.e. when you use a value of the wrong type for an operation. Example: print("Age: " + 25) or list[0.5]
  • ValueError happens when you're dealing with values which may be inappropriately used in some context. In general, this exception is raised when a function (like int() or float()) receives an argument of a proper type, but its value is unacceptable/doesn’t make sense. Example: int("hello")"hello" is a string, but not a valid number.
  • ZeroDivisionError happens when you try to force Python to perform any operation which provokes division in which the divider is zero, or is indistinguishable from zero. Note that there is more than one Python operator which may cause this exception to raise: /, //, %.

Exceptions

  • KeyboardInterrupt raised when the user hits the interrupt key (CTRL-C or Delete) i.e. when you manually stop a running Python program—usually by pressing Ctrl+C.

Cool things to keep in mind

  • print() can handle octals and hexadecimals
  • 0.4 can be written as .4 and 4.0 can be written as 4. because you can omit zero when it is the only digit in front of or after the decimal point
  • 300000000 or 3 x 10⁸ scientific notation is written as 3e8 or 3E8 in Python. Ex.: 6.62607E-34
    • Note: the exponent (the value after the E) has to be an integer; the base (the value in front of the E) may be either an integer or a float.
  • print('I\'m Monty Python.') or print("I'm Monty Python.")
  • 6 // 3 equals 2 (integer) because only when an integer is divided by an integer with // (integer/floor division) it results in an integer. Any regular divisions (/) or a floor division involving a float operand WILL result in a float because anything involving a float results in a float, even addition, subtraction, multiplication and exponentiation. Even an integer exponentiation may result in a float: 4 ** -1 equals 0.25. However, for //, anything after the floating point becomes 0. Ex.: 10 / 3 equals 3.333... but 10 // 3 equals 3
  • % (Remainder/Modulo) always results in a integer (or a float with .0 if either operand is a float) because it results the remainder left after the integer division
>OPERATION INT & INT INT & FLOAT FLOAT & INT FLOAT & FLOAT
Addition 1 + 1 = 2 1 + 1.0 = 2.0 1.5 + 1 = 2.5 1.5 + 1.0 = 2.5
Subtraction 1 - 1 = 0 1 - 1.0 = 0.0 1.5 - 1 = 0.5 1.5 - 1.0 = 0.5
Multiplication 2 * 2 = 4 2 * 2.0 = 4.0 2.5 * 2 = 5.0 2.5 * 2.0 = 5.0
Division 2 / 2 = 1.0 2 / 2.0 = 1.0 2.5 / 2 = 1.25 2.5 / 2.0 = 1.25
Floor Division 2 // 2 = 1 2 // 2.0 = 1.0 2.5 // 2 = 1.0 2.5 // 2.0 = 1.0
Modulo 12 % 5 = 2 12 % 4.5 = 3.0 12.0 % 5 = 2.0 12.5 % 4.5 = 3.5
Exponentiation 4 ** 2 = 16 4 ** 2.0 = 16.0 4.5 ** 2 = 20.25 4.5 ** 2.0 = 20.25
Extra 4 ** -1 = 0.25
  • 2 % 4 equals 2 because 2 - 4 * (2 // 4) = 2 - 4 * 0 = 2 - 0 = 2
  • 2 % -4 equals -2 because 2 - (-4) * (2 // -4) = 2 - (-4) * -1.0 = 2 - 4 = -2
  • if a = 6 and b = 3 then a /= 2 * b is 1.0 because a = a / (2 * b)
  • 9 ** 0.5 evaluates the square root of 9
  • 9 * 0.1 evaluates 10% of 9
  • 9 * 1.1 evaluates 9 + 10% of 9
  • input() can be used without an argument
  • 2 == 2. | 2. == 2 is True
  • while number: is the same as while number != 0:
  • if number % 2: is the same as if number % 2 == 1:
  • Data Types and boolean values:
print(
f'''Integer: 1 - {bool(1)} | 0 - {bool(0)} | -1 - {bool(-1)}
Floating point: 1.0 - {bool(1.0)} | 0.0 - {bool(0.0)} | 0.1 - {bool(0.1)} | -1.0 - {bool(-1.0)}
String: 'Test' - {bool('Test')} | '' - {bool('')} | '0' - {bool('0')}
NoneType: None - {bool(None)}
List: [0, 0.0, '', False] - {bool([0, 0.0, '', False])} | [] - {bool([])}
Tuple: (0, 0.0, '', False) - {bool((0, 0.0, '', False))} | () - {bool(())}
Set: {{0, 0.0, '', False}} - {bool({0, 0.0, '', False})} | {{}} - {bool({})} | set() - {bool(set())}
''')

a = False
if a is not None:
    print(f"{a} is not NULL")
if a:
    print(f"{a} is not NULL, nor EMPTY (e.g.: '', 0, 0.0, False)")
  • Python == vs is:
    • == → checks value equality
    • is → checks object identity (same memory location)
    • For booleans, always use is when checking against True or False after using bool().
    • Never use is for numbers — only for singletons like None, True, False.
Expression Result Explanation
0 == False True Values are equal (False is int subclass with value 0)
0 is False False Different objects/types (int vs bool)
bool(0) == False True Value equality, 0 converts to False
bool(0) is False True bool() returns the canonical False object → same identity
None == None True Values are equal
None is None True Identity check → same object in memory
len([]) == 0 True len([]) returns 0, equal to 0
len([]) is 0 False (unsafe to rely on) Different objects, is checks identity
len([]) == False True 0 is treated as False in boolean context
len([]) is False False different object types (int vs bool)
len([]) is None False different object types
  • ~4 equals -5 because of two's complement: - (4 + 1)
  • 17 >> 1 → 17 // 2 (17 floor-divided by 2 to the power of 1) → 8 (shifting to the right by one bit is the same as integer division by two)
  • 17 << 2 → 17 * 4 (17 multiplied by 2 to the power of 2) → 68 (shifting to the left by two bits is the same as integer multiplication by four)
  • del numbers[1] removes the 2nd element of a list while del my_list deletes the whole list
  • An element with an index equal to -1 is the last one in the list.. Ex.: numbers[-1]
  • numbers.append(4) adds the number 4 at the end of the list
  • numbers.insert(0, 222) adds the number 222 to the index 0 of the list
  • numbers.sort() sorts the list using timsort in ascending order
  • numbers.reverse() reverses the list order, but without sorting it at all

How to reverse the order of certain pairs of numbers in a list:

for i in range(length // 2):
    my_list[i], my_list[length - i - 1] = my_list[length - i - 1], my_list[i]
 
print(my_list)

Note:

  • we've assigned the length variable with the current list's length (this makes our code a bit clearer and shorter)
  • we've launched the for loop to run through its body length // 2 times (this works well for lists with both even and odd lengths, because when the list contains an odd number of elements, the middle one remains untouched)
  • we've swapped the ith element (from the beginning of the list) with the one with an index equal to (length - i - 1) (from the end of the list); in our example, for i equal to 0 the (length - i - 1) gives 4; for i equal to 1, it gives 3 ‒ this is exactly what we needed.
  • my_list[start:end] is called slicing which selects values from a list to work upon. start is the index of the first element included in the slice; end is the index of the first element not included in the slice. If you omit the start in your slice, it is assumed that you want to get a slice beginning at the element with index 0. Similarly, if you omit the end in your slice, it is assumed that you want the slice to end at the element with the index len(my_list).

  • my_list[start:] means my_list[start:len(my_list)] so considering my_list = [10, 8, 6, 4, 2], new_list = my_list[3:] equals [4, 2]

  • my_list[:end] means my_list[0:end] so considering the above list new_list = my_list[:3] equals [10, 8, 6]

  • list_1[:] selects an entire list. my_list[1:3] is more precise about where to start and end.

  • my_list[1:-1] while -1 represents the last element in a list

  • my_list[-1:1] equals []

  • import mod1 or import mod2, mod3, mod4(latter not recommended due to stylistic reasons) - imports a whole module, each entity is accessed by using module.entity(). Here the entity name must be prefixed by the module name. None of the imported entity names conflicts with the identical names existing in your code's namespace.

  • from module import entity - imports a single entity from a module which can be used by writing element(). In this case, the imported entities must not be prefixed when used. This is not recommended because of the danger of causing conflicts with names derived from importing the code's namespace.

  • from module import * - imports all entities offered by a module, each entity is accessed by using entity(). Note: this import's variant is not recommended due to the same reasons as previously (the threat of a naming conflict is even more dangerous here).

  • import module as m or from module import name as alias - You can change the name of the imported entity "on the fly" by using the as phrase of the import which is a way to avoid a remote module entity clashing with local entities. This is called aliasing and can be done on both the module and its entities.

  • from module import element1 as e1, element2 as e2, element3 as e3 - more all at once, not recommended because can get too long. Instead, do each import one by line.

  • import module then dir(module) returns an alphabetically sorted list containing all entities' names available in the module identified by a name passed to the function as an argument

  • Considering from math import *

    • The first group of the math's functions are connected with trigonometry. All these functions take one argument (an angle measurement expressed in radians) and return the appropriate result (be careful with tan() - not all arguments are accepted):
      • sin(x) → the sine of x;
      • cos(x) → the cosine of x;
      • tan(x) → the tangent of x.
    • Of course, there are also their inversed versions. These functions take one argument (mind the domains) and return a measure of an angle in radians:
      • asin(x) → the arcsine of x;
      • acos(x) → the arccosine of x;
      • atan(x) → the arctangent of x.
    • To effectively operate on angle measurements, the math module provides you with the following entities:
      • pi → a constant with a value that is an approximation of π;
      • radians(x) → a function that converts x from degrees to radians;
      • degrees(x) → acting in the other direction (from radians to degrees)
    • Apart from the circular functions (listed above) the math module also contains a set of their hyperbolic analogues:
      • sinh(x) → the hyperbolic sine;
      • cosh(x) → the hyperbolic cosine;
      • tanh(x) → the hyperbolic tangent;
      • asinh(x) → the hyperbolic arcsine;
      • acosh(x) → the hyperbolic arccosine;
      • atanh(x) → the hyperbolic arctangent.
    • Another group of the math's functions is formed by functions which are connected with exponentiation:
      • e → a constant with a value that is an approximation of Euler's number (e)
      • exp(x) → finding the value of e to the power of x
      • log(x) → the natural logarithm of x
      • log(x, b) → the logarithm of x to base b
      • log10(x) → the decimal logarithm of x (more precise than log(x, 10))
      • log2(x) → the binary logarithm of x (more precise than log(x, 2))
      • Note: the pow() function which is a built-in function, and doesn't have to be imported:
        • pow(x, y) → finding the value of x to the power of y (mind the domains)
    • The last group consists of some general-purpose functions like:
      • ceil(x) → the ceiling of x (the smallest integer greater than or equal to x)
      • floor(x) → the floor of x (the largest integer less than or equal to x)
      • trunc(x) → the value of x truncated to an integer (be careful - it's not an equivalent either of ceil or floor)
      • factorial(x) → returns x! (x has to be an integral and not a negative)
      • hypot(x, y) → returns the length of the hypotenuse of a right-angle triangle with the leg lengths equal to x and y (the same as sqrt(pow(x, 2) + pow(y, 2)) but more precise)
from math import pi, radians, degrees, sin, cos, tan, asin

ad = 90
ar = radians(ad)
ad = degrees(ar)

print(ad == 90.)  # Output: True
print(ar == pi / 2.)  # Output: True
print(sin(ar) / cos(ar) == tan(ar))  # Output: True
print(asin(sin(ar)) == ar)  # Output: True


from math import e, exp, log

print(pow(e, 1) == exp(log(e)))  # Output: False
print(pow(2, 2) == exp(2 * log(2)))  # Output: True
print(log(e, e) == exp(0))  # Output: True


from math import ceil, floor, trunc

x = 1.4
y = 2.6

print(floor(x), floor(y))  # Output: 1 2
print(floor(-x), floor(-y))  # Output: -2 -3
print(ceil(x), ceil(y))  # Output: 2 3
print(ceil(-x), ceil(-y))  # Output: -1 -2
print(trunc(x), trunc(y))  # Output: 1 2
print(trunc(-x), trunc(-y))  # Output: -1 -2

[!INFO]- About pseudorandom numbers Note the prefix pseudo - the numbers generated by the modules may look random in the sense that you cannot predict their subsequent values, but don't forget that they all are calculated using very refined algorithms.

The algorithms aren't random - they are deterministic and predictable. Only those physical processes which run completely out of our control (like the intensity of cosmic radiation) may be used as a source of actual random data. Data produced by deterministic computers cannot be random in any way.

A random number generator takes a value called a seed, treats it as an input value, calculates a "random" number based on it (the method depends on a chosen algorithm) and produces a new seed value.

The length of a cycle in which all seed values are unique may be very long, but it isn't infinite - sooner or later the seed values will start repeating, and the generating values will repeat, too. This is normal. It's a feature, not a mistake, or a bug.

The initial seed value, set during the program start, determines the order in which the generated values will appear.

The random factor of the process may be augmented by setting the seed with a number taken from the current time - this may ensure that each program launch will start from a different seed value (ergo, it will use different random numbers).

Fortunately, such an initialization is done by Python during module import.

  • Considering from random import * which delivers some mechanisms allowing you to operate with pseudorandom numbers
    • random() produces a float number x coming from the range (0.0, 1.0) - in other words: (0.0 <= x < 1.0). x's value is determined by the current (rather unpredictable) seed value. Ex.: 0.9535768927411208 then 0.5312710096244534 then 0.8737691983477731, etc, all pseudorandom.
    • seed() is able to directly set the generator's seed. We'll show you two of its variants:
      • seed() - sets the seed with the current time;
      • seed(int_value) - sets the seed with the integer value int_value.
      • IMPORTANT: If the seed is always set with the same value, the sequence of generated values always looks the same. Example: seed is set with seed(0) then the results are 0.844421851525then 0.75795440294, then 0.420571580831 then 0.258916750293 then 0.511274721369, etc, always.
    • randrange() returns a random integer. These invocations will generate an integer taken (pseudorandomly) from the range (respectively). Note the implicit right-sided exclusion!:
      • randrange(end). Example: print(randrange(1)) will always generate 0 because 1 is excluded as the last range number always is
      • randrange(beg, end) Example: print(randrange(0, 1)) will always generate 0 because 1 is still excluded
      • randrange(beg, end, step). Example: print(randrange(0, 1, 1)) will always generate 0 because 1 is still excluded and the last 1 is the default value
    • randint() also returns a random integer but it doesn't exclude the last value as it generates the integer value i, which falls in the range [left, right] (no exclusion on the right side) which is an equivalent of randrange(left, right+1). Example: print(randint(0, 1)) may generate either 0 or 1
    • choice(sequence) chooses a "random" element from the input sequence and returns it.
      • Example: considering my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], then print(choice(my_list)) results in a random element of the list
    • sample(sequence, elements_to_choose) builds a list (a sample) consisting of the elements_to_choose element drawn from the input sequence. In other words, the function chooses some of the input elements, returning a list with the choice. The elements in the sample are placed in random order. Note: the elements_to_choose must not be greater than the length of the input sequence.
      • Example: considering my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], then print(sample(my_list, 5)) results in a list containing 5 random elements of the input list
      • Example: considering my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], then print(sample(my_list, 10)) results in a list containing 10 random elements of that list
  • Considering from platform import * (which module lets you access the underlying platform's data, i.e., hardware, operating system, and interpreter version information)
    • platform(aliased = False, terse = False) shows the OS with its version. The arguments are optional but can change its behavior:
      • aliased → when set to True (or any non-zero value) it may cause the function to present the alternative underlying layer names instead of the common ones;
      • terse → when set to True (or any non-zero value) it may convince the function to present a briefer form of the result (if possible)
      • Examples:
        • print(platform()) gives Linux-4.4.0-1-rpi2-armv7l-with-debian-9.0 or Windows-Vista-6.0.6002-SP2
        • print(platform(1)) gives Linux-4.4.0-1-rpi2-armv7l-with-debian-9.0 or Windows-Vista-6.0.6002-SP2
        • print(platform(0, 1)) gives Linux-4.4.0-1-rpi2-armv7l-with-glibc2.9 or Windows-Vista
    • machine() shows the generic name of the processor which runs your OS together with Python and your code
      • Example: x86_64 or armv7l
    • processor() returns a string filled with the real processor name (if possible)
      • Example: Intel(R) Core(TM) i3-2330M CPU @ 2.20GHz or armv7l

How to read Python:

var = 1
print(var)
var = var + 1
print(var)
  • The first line of the snippet creates a new variable named var and assigns 1 to it.
  • The statement reads: assign a value of 1 to a variable named var.
  • We can say it shorter: assign 1 to var.
  • Some prefer to read such a statement as: var becomes 1.
  • The third line assigns the same variable with the new value taken from the variable itself, summed with 1. Seeing a record like that, a mathematician would probably protest ‒ no value may be equal to itself plus one. This is a contradiction. But Python treats the sign = not as equal to, but as assign a value.
  • Take the current value of the variable var, add 1 to it and store the result in the variable var.
  • In effect, the value of variable var has been incremented by one, which has nothing to do with comparing the variable with any value.

function_name(argument)

  • First, Python checks if the name specified is legal (it browses its internal data in order to find an existing function of the name; if this search fails, Python aborts the code)
  • second, Python checks if the function's requirements for the number of arguments allows you to invoke the function in this way (e.g., if a specific function demands exactly two arguments, any invocation delivering only one argument will be considered erroneous, and will abort the code's execution)
  • third, Python leaves your code for a moment and jumps into the function you want to invoke; of course, it takes your argument(s) too and passes it/them to the function;
  • fourth, the function executes its code, causes the desired effect (if any), evaluates the desired result(s) (if any) and finishes its task;
  • finally, Python returns to your code (to the place just after the invocation) and resumes its execution.
print("Tell me anything...")
anything = input()
print("Hmm...", anything"... Really?"

It shows a very simple case of using the input() function.

Note:

  • The program prompts the user to input some data from the console (most likely using a keyboard, although it is also possible to input data using voice or image);
  • the input() function is invoked without arguments (this is the simplest way of using the function); the function will switch the console to input mode; you'll see a blinking cursor, and you'll be able to input some keystrokes, finishing off by hitting the Enter key; all the inputted data will be sent to your program through the function's result;
  • note: you need to assign the result to a variable; this is crucial ‒ missing out this step will cause the entered data to be lost;
  • then we use the print() function to output the data we get, with some additional remarks.
anything = input("Tell me anything...")
print("Hmm...", anything"...Really?"

Note:

  • the input() function is invoked with one argument ‒ it's a string containing a message;
  • the message will be displayed on the console before the user is given an opportunity to enter anything;
  • input() will then do its job. This variant of the input() invocation simplifies the code and makes it clearer.
if true_or_not:
    do_this_if_true 

This conditional statement consists of the following, strictly necessary, elements in this and this order only:

  • the if keyword;
  • one or more white spaces;
  • an expression (a question or an answer) whose value will be interpreted solely in terms of True (when its value is non-zero) and False (when it is equal to zero);
  • colon followed by a newline;
  • an indented instruction or set of instructions (at least one instruction is absolutely required); the indentation may be achieved in two ways – by inserting a particular number of spaces (the recommendation is to use four spaces of indentation), or by using the tab character; note: if there is more than one instruction in the indented part, the indentation should be the same in all lines; even though it may look the same if you use tabs mixed with spaces, it's important to make all indentations exactly the same – Python 3 does not allow the mixing of spaces and tabs for indentation.

How does that statement work?

  • If the true_or_not expression represents the truth (i.e., its value is not equal to zero), the indented statement(s) will be executed;
  • if the true_or_not expression does not represent the truth (i.e., its value is equal to zero), the indented statement(s) will be omitted (ignored), and the next executed instruction will be the one after the original indentation level.

In real life, we often express a desire:

if the weather is good, we'll go for a walk then, we'll have lunch

As you can see, having lunch is not a conditional activity and doesn't depend on the weather.

Knowing what conditions influence our behavior, and assuming that we have the parameterless functions go_for_a_walk() and have_lunch(), we can write the following snippet:

if the_weather_is_good:
    go_for_a_walk()
have_lunch()

If a certain sleepless Python developer falls asleep when he or she counts 120 sheep, and the sleep-inducing procedure may be implemented as a special function named sleep_and_dream(), the whole code takes the following shape:

if sheep_counter >= 120# Evaluate a test expression
    sleep_and_dream() # Execute if test expression is True` 

You can read it as: if sheep_counter is greater than or equal to 120, then fall asleep and dream (i.e., execute the sleep_and_dream function.)

We can say, for example: If the weather is good, we will go for a walk, otherwise we will go to a theater.

Now we know what we'll do if the conditions are met, and we know what we'll do if not everything goes our way. In other words, we have a "Plan B".

Python allows us to express such alternative plans. This is done with a second, slightly more complex form of the conditional statement, the if-else statement:

if true_or_false_condition:
    perform_if_condition_true
else:
    perform_if_condition_false

Thus, there is a new word: else – this is a keyword.

The part of the code which begins with else says what to do if the condition specified for the if is not met (note the colon after the word).

The if-else execution goes as follows:

  • if the condition evaluates to True (its value is not equal to zero), the perform_if_condition_true statement is executed, and the conditional statement comes to an end;
  • if the condition evaluates to False (it is equal to zero), the perform_if_condition_false statement is executed, and the conditional statement comes to an end.

By using this form of conditional statement, we can describe our plans as follows:

if the_weather_is_good:
    go_for_a_walk()
else:
    go_to_a_theater()
have_lunch()

If the weather is good, we'll go for a walk. Otherwise, we'll go to a theater. No matter if the weather is good or bad, we'll have lunch afterwards (after the walk or after going to the theater).

Read what we have planned for this Sunday. If the weather is fine, we'll go for a walk. If we find a nice restaurant, we'll have lunch there. Otherwise, we'll eat a sandwich. If the weather is poor, we'll go to the theater. If there are no tickets, we'll go shopping in the nearest mall.

Let's write the same in Python. Consider carefully the code here:

if the_weather_is_good:
    if nice_restaurant_is_found:
        have_lunch()
    else:
        eat_a_sandwich()
else:
    if tickets_are_available:
        go_to_the_theater()
    else:
        go_shopping()

elif is used to check more than just one condition, and to stop when the first statement which is true is found.

Our next example resembles nesting, but the similarities are very slight. Again, we'll change our plans and express them as follows: If the weather is fine, we'll go for a walk, otherwise if we get tickets, we'll go to the theater, otherwise if there are free tables at the restaurant, we'll go for lunch; if all else fails, we'll stay home and play chess.

Have you noticed how many times we've used the word otherwise? This is the stage where the elif keyword plays its role.

Let's write the same scenario using Python:

if the_weather_is_good:
    go_for_a_walk()
elif tickets_are_available:
    go_to_the_theater()
elif table_is_available:
    go_for_lunch()
else:
    play_chess_at_home()

Actually, the for loop is designed to do more complicated tasks – it can "browse" large collections of data item by item. We'll show you how to do that soon, but right now we're going to present a simpler variant of its application.

Take a look at the snippet:

for i in range(100):
    # do_something()
    pass

There are some new elements. Let us tell you about them:

  • the for keyword opens the for loop; note – there's no condition after it; you don't have to think about conditions, as they're checked internally, without any intervention;
  • any variable after the for keyword is the control variable of the loop; it counts the loop's turns, and does it automatically;
  • the in keyword introduces a syntax element describing the range of possible values being assigned to the control variable;
  • the range() function (this is a very special function) is responsible for generating all the desired values of the control variable; in our example, the function will create (we can even say that it will feed the loop with) subsequent values from the following set: 0, 1, 2 .. 97, 98, 99; note: in this case, the range() function starts its job from 0 and finishes it one step (one integer number) before the value of its argument;
  • note the pass keyword inside the loop body – it does nothing at all; it's an empty instruction – we put it here because the for loop's syntax demands at least one instruction inside the body (by the way – ifelifelse and while express the same thing)

Let's create a variable called numbers; it's assigned with not just one number, but is filled with a list consisting of five values (note: the list starts with an open square bracket and ends with a closed square bracket; the space between the brackets is filled with five numbers separated by commas).

numbers = [10, 5, 7, 2, 1] 

Let's say the same thing using adequate terminology: numbers is a list consisting of five values, all of them numbers. We can also say that this statement creates a list of length equal to five (as in there are five elements inside it).

The for loop has a special variant that can process lists very effectively ‒ let's take a look at that.

my_list = [10, 1, 8, 3, 5]
total = 0

for i in range(len(my_list)):
    total += my_list[i]

print(total)

Let's assume that you want to calculate the sum of all the values stored in the my_list list.

You need a variable whose sum will be stored and initially assigned a value of 0 ‒ its name will be total. (Note: we're not going to name it sum as Python uses the same name for one of its built-in functions: sum()Using the same name would generally be considered bad practice.) Then you add to it all the elements of the list using the for loop. Take a look at the snippet in the editor.

Let's comment on this example:

  • the list is assigned a sequence of five integer values;
  • the i variable takes the values 01, 23, and 4, and then it indexes the list, selecting the subsequent elements: the first, second, third, fourth and fifth;
  • each of these elements is added together by the += operator to the total variable, giving the final result at the end of the loop;
  • note the way in which the len() function has been employed ‒ it makes the code independent of any possible changes in the list's contents.

But the for loop can do much more. It can hide all the actions connected to the list's indexing, and deliver all the list's elements in a handy way.

This modified snippet shows how it works:

my_list = [10, 1, 8, 3, 5]
total = 0

for i in my_list:
    total += i

print(total)

What happens here?

  • the for instruction specifies the variable used to browse the list (i here) followed by the in keyword and the name of the list being processed (my_list here)
  • the i variable is assigned the values of all the subsequent list's elements, and the process occurs as many times as there are elements in the list;
  • this means that you use the i variable as a copy of the elements' values, and you don't need to use indices;
  • the len() function is not needed here, either.

Now you can easily swap the list's elements to reverse their order:

my_list = [10, 1, 8, 3, 5]

my_list[0], my_list[4] = my_list[4], my_list[0]
my_list[1], my_list[3] = my_list[3], my_list[1]

print(my_list)

Run the snippet. Its output should look like this:

[5, 3, 8, 1, 10]

It looks fine with five elements.

Will it still be acceptable with a list containing 100 elements? No, it won't.

Can you use the for loop to do the same thing automatically, irrespective of the list's length? Yes, you can.

This is how we've done it:

for i in range(length // 2):
    my_list[i], my_list[length - i - 1] = my_list[length - i - 1], my_list[i]

print(my_list)

Note:

  • we've assigned the length variable with the current list's length (this makes our code a bit clearer and shorter)
  • we've launched the for loop to run through its body length // 2 times (this works well for lists with both even and odd lengths, because when the list contains an odd number of elements, the middle one remains untouched)
  • we've swapped the ith element (from the beginning of the list) with the one with an index equal to (length - i - 1) (from the end of the list); in our example, for i equal to 0 the (length - i - 1) gives 4; for i equal to 1, it gives 3 ‒ this is exactly what we needed.
list_1 = [1]
list_2 = list_1
list_1[0] = 2
print(list_2)

The program:

  • creates a one-element list named list_1;
  • assigns it to a new list named list_2;
  • changes the only element of list_1;
  • prints out list_2.

The surprising part is the fact that the program will output: [2], not [1], which seems to be the obvious solution.

Lists (and many other complex Python entities) are stored in different ways than ordinary (scalar) variables.

You could say that:

  • the name of an ordinary variable is the name of its content;
  • the name of a list is the name of a memory location where the list is stored.

Read these two lines once more ‒ the difference is essential for understanding what we are going to talk about next.

The assignment: list_2 = list_1 copies the name of the array, not its contents. In effect, the two names (list_1 and list_2) identify the same location in the computer memory. Modifying one of them affects the other, and vice versa.

How do you cope with that?

Python offers two very powerful operators, able to look through the list in order to check whether a specific value is stored inside the list or not.

These operators are:

elem in my_list
elem not in my_list

The first of them (in) checks if a given element (its left argument) is currently stored somewhere inside the list (the right argument) ‒ the operator returns True in this case.

The second (not in) checks if a given element (its left argument) is absent in a list ‒ the operator returns True in this case.

Look at the code below:

row = []

for i in range(8):
    row.append(WHITE_PAWN)

It builds a list containing eight elements representing the second row of the chessboard ‒ the one filled with pawns (assume that WHITE_PAWN is a predefined symbol representing a white pawn).

Take a look at the snippet:

row = [WHITE_PAWN for i in range(8)]

The part of the code placed inside the brackets specifies:

  • the data to be used to fill the list (WHITE_PAWN)
  • the clause specifying how many times the data occurs inside the list (for i in range(8))

You can use nested lists in Python to create matrices (i.e., two-dimensional lists). For example:

# A four-column/four-row table ‒ a two dimensional array (4x4)

table = [[":(", ":)", ":(", ":)"],
         [":)", ":(", ":)", ":)"],
         [":(", ":)", ":)", ":("],
         [":)", ":)", ":)", ":("]]

print(table)
print(table[0][0])  # outputs: ':('
print(table[0][3])  # outputs: ':)'

You can nest as many lists in lists as you want, thereby creating n-dimensional lists, e.g., three-, four- or even sixty-four-dimensional arrays. For example:

# Cube - a three-dimensional array (3x3x3)

cube = [[[':(', 'x', 'x'],
         [':)', 'x', 'x'],
         [':(', 'x', 'x']],

        [[':)', 'x', 'x'],
         [':(', 'x', 'x'],
         [':)', 'x', 'x']],

        [[':(', 'x', 'x'],
         [':)', 'x', 'x'],
         [':)', 'x', 'x']]]

print(cube)
print(cube[0][0][0])  # outputs: ':('
print(cube[2][2][0])  # outputs: ':)'

You need to define it. The word define is significant here.

This is what the simplest function definition looks like:

def function_name():
    function_body 
  • It always starts with the keyword def (for define)
  • next after def goes the name of the function (the rules for naming functions are exactly the same as for naming variables)
  • after the function name, there's a place for a pair of parentheses (they contain nothing here, but that will change soon)
  • the line has to be ended with a colon;
  • the line directly after def begins the function body ‒ a couple (at least one) of necessarily nested instructions, which will be executed every time the function is invoked; note: the function ends where the nesting ends, so you have to be careful.

IMPORTANT: Python reads the function's definitions and remembers them, but won't launch any of them without your permission, so make sure to insert the function's invocation in the code.

  • when you invoke a function, Python remembers the place where it happened and jumps into the invoked function;
  • the body of the function is then executed;
  • reaching the end of the function forces Python to return to the place directly after the point of invocation.

Look at the function invocation below:

adding(3, c = 1, b = 2)

Let's analyze it:

  • the argument (3) for the a parameter is passed using the positional way;
  • the arguments for c and b are specified as keyword ones.

This is what you'll see in the console:

3 + 2 + 1 = 6

"Okay," you may say now, 'but how should I beg for forgiveness when the program is terminated and there is nothing left that can be done?" This is where the exception comes on the scene.

try:
	# It's a place where
	# you can do something 
    # without asking for permission.
except:
	# It's a spot dedicated to 
    # solemnly begging for forgiveness.

You can see two branches here:

  • first, starting with the try keyword – this is the place where you put the code you suspect is risky and may be terminated in case of error; note: this kind of error is called an exception, while the exception occurrence is called raising – we can say that an exception is (or was) raised;
  • second, the part of the code starting with the except keyword is designed to handle the exception; it's up to you what you want to do here: you can clean up the mess or you can just sweep the problem under the carpet (although we would prefer the first solution).

So, we could say that these two blocks work like this:

  • the try keyword marks the place where you try to do something without permission;
  • the except keyword starts a location where you can show off your apology talents.

As you can see, this approach accepts errors (treats them as a normal part of the program's life) instead of escalating efforts to avoid errors at all.

try:
    value = int(input('Enter a natural number: '))
    print('The reciprocal of', value, 'is', 1/value)        
except ValueError:
    print('I do not know what to do.')
    

Let us summarize what we talked about:

  • any part of the code placed between try and except is executed in a very special way – any error which occurs here won't terminate program execution. Instead, the control will immediately jump to the first line situated after the except keyword, and no other part of the try branch is executed;
  • the code in the except branch is activated only when an exception has been encountered inside the try block. There is no way to get there by any other means;
  • when either the try block or the except block is executed successfully, the control returns to the normal path of execution, and any code located beyond in the source file is executed as if nothing happened.

Now we want to ask you an innocent question: is ValueError the only way the control could fall into the except branch?

You can "catch" and handle exceptions in Python by using the try-except block. So, if you have a suspicion that any particular snippet may raise an exception, you can write the code that will gracefully handle it, and will not interrupt the program. Look at the example:

while True:
    try:
        number = int(input("Enter an integer number: "))
        print(number/2)
        break
    except:
        print("Warning: the value entered is not a valid number. Try again...")

The code above asks the user for input until they enter a valid integer number. If the user enters a value that cannot be converted to an int, the program will print Warning: the value entered is not a valid number. Try again..., and ask the user to enter a number again. What happens in such a case?

  1. The program enters the while loop.
  2. The try block/clause is executed. The user enters a wrong value, for example: hello!.
  3. An exception occurs, and the rest of the try clause is skipped. The program jumps to the except block, executes it, and then continues running after the try-except block.

If the user enters a correct value and no exception occurs, the subsequent instructions in the try block are executed.

You can also specify and handle multiple built-in exceptions within a single except clause:

while True:
    try:
        number = int(input("Enter an int number: "))
        print(5/number)
        break
    except (ValueError, ZeroDivisionError):
        print("Wrong value or No division by zero rule broken.")
    except:
        print("Sorry, something went wrong...")

Examples

# Read two numbers
number1 = int(input("Enter the first number: "))
number2 = int(input("Enter the second number: "))

# Choose the larger number
if number1 > number2:
    larger_number = number1
else:
    larger_number = number2

# Print the result
print("The larger number is:", larger_number)
# Read three numbers
number1 = int(input("Enter the first number: "))
number2 = int(input("Enter the second number: "))
number3 = int(input("Enter the third number: "))

# We temporarily assume that the first number
# is the largest one.
# We will verify this soon.
largest_number = number1

# We check if the second number is larger than the current largest_number
# and update the largest_number if needed.
if number2 > largest_number:
    largest_number = number2

# We check if the third number is larger than the current largest_number
# and update the largest_number if needed.
if number3 > largest_number:
    largest_number = number3

# Print the result
print("The largest number is:", largest_number)
# Store the current largest number here.
largest_number = -999999999

# Input the first value.
number = int(input("Enter a number or type -1 to stop: "))

# If the number is not equal to -1, continue.
while number != -1:
    # Is number larger than largest_number?
    if number > largest_number:
        # Yes, update largest_number.
        largest_number = number
    # Input the next number.
    number = int(input("Enter a number or type -1 to stop: "))

# Print the largest number.
print("The largest number is:", largest_number)
largest_number = -99999999
counter = 0

while True:
    number = int(input("Enter a number or type -1 to end the program: "))
    if number == -1:
        break
    counter += 1
    if number > largest_number:
        largest_number = number

if counter != 0:
    print("The largest number is", largest_number)
else:
    print("You haven't entered any number.")
largest_number = -99999999
counter = 0

number = int(input("Enter a number or type -1 to end program: "))

while number != -1:
    if number == -1:
        continue
    counter += 1

    if number > largest_number:
        largest_number = number
    number = int(input("Enter a number or type -1 to end the program: "))

if counter:
    print("The largest number is", largest_number)
else:
    print("You haven't entered any number.")

# A program that reads a sequence of numbers
# and counts how many numbers are even and how many are odd.
# The program terminates when zero is entered.

odd_numbers = 0
even_numbers = 0

# Read the first number.
number = int(input("Enter a number or type 0 to stop: "))

# 0 terminates execution.
while number != 0:
    # Check if the number is odd.
    if number % 2 == 1:
        # Increase the odd_numbers counter.
        odd_numbers += 1
    else:
        # Increase the even_numbers counter.
        even_numbers += 1
    # Read the next number.
    number = int(input("Enter a number or type 0 to stop: "))

# Print results.
print("Odd numbers count:", odd_numbers)
print("Even numbers count:", even_numbers)
counter = 5
while counter != 0:
    print("Inside the loop.", counter)
    counter -= 1
print("Outside the loop.", counter)
counter = 5
while counter:
    print("Inside the loop.", counter)
    counter -= 1
print("Outside the loop.", counter)
i = 1
while i < 5:
    print(i)
    i += 1
else:
    print("else:", i)
i = 5
while i < 5:
    print(i)
    i += 1
else:
    print("else:", i)
for i in range(5):
    print(i)
else:
    print("else:", i)
i = 111
for i in range(2, 1):
    print(i)
else:
    print("else:", i)
n = 3
 
while n > 0:
    print(n + 1)
    n -= 1
else:
    print(n)
n = range(4)
 
for num in n:
    print(num - 1)
else:
    print(num)
my_list = []  # Creating an empty list.

for i in range(5):
    my_list.append(i + 1)

print(my_list)
my_list = []  # Creating an empty list.

for i in range(5):
    my_list.insert(0, i + 1)

print(my_list)

my_list = [8, 10, 6, 2, 4]  # list to sort

for i in range(len(my_list) - 1):  # we need (5 - 1) comparisons
    if my_list[i] > my_list[i + 1]:  # compare adjacent elements
        my_list[i], my_list[i + 1] = my_list[i + 1], my_list[i]  # If we end up here, we have to swap the elements.
my_list = [8, 10, 6, 2, 4]  # list to sort
swapped = True  # It's a little fake, we need it to enter the while loop.

while swapped:
    swapped = False  # no swaps so far
    for i in range(len(my_list) - 1):
        if my_list[i] > my_list[i + 1]:
            swapped = True  # a swap occurred!
            my_list[i], my_list[i + 1] = my_list[i + 1], my_list[i]

print(my_list)
my_list = []
swapped = True
num = int(input("How many elements do you want to sort: "))

for i in range(num):
    val = float(input("Enter a list element: "))
    my_list.append(val)

while swapped:
    swapped = False
    for i in range(len(my_list) - 1):
        if my_list[i] > my_list[i + 1]:
            swapped = True
            my_list[i], my_list[i + 1] = my_list[i + 1], my_list[i]

print("\nSorted:")
print(my_list)

my_list = [17, 3, 11, 5, 1, 9, 7, 15, 13]
largest = my_list[0]

for i in range(1, len(my_list)):
    if my_list[i] > largest:
        largest = my_list[i]

print(largest)

The concept is rather simple ‒ we temporarily assume that the first element is the largest one, and check the hypothesis against all the remaining elements in the list.

The code outputs 17 (as expected).

The code may be rewritten to make use of the newly introduced form of the for loop:

my_list = [17, 3, 11, 5, 1, 9, 7, 15, 13]
largest = my_list[0]

for i in my_list:
    if i > largest:
        largest = i

print(largest)

The program above performs one unnecessary comparison, when the first element is compared with itself, but this isn't a problem at all.

The code outputs 17, too (nothing unusual).

If you need to save computer power, you can use a slice:

my_list = [17, 3, 11, 5, 1, 9, 7, 15, 13]
largest = my_list[0]

for i in my_list[1:]:
    if i > largest:
        largest = i

print(largest)

Now let's find the location of a given element inside a list:

my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
to_find = 5
found = False

for i in range(len(my_list)):
    found = my_list[i] == to_find
    if found:
        break

if found:
    print("Element found at index", i)
else:
    print("absent")

Note:

  • the target value is stored in the to_find variable;
  • the current status of the search is stored in the found variable (True/False)
  • when found becomes True, the for loop is exited.

Let's assume that you've chosen the following numbers in the lottery: 3711423449.

The numbers that have been drawn are: 511942349.

The question is: how many numbers have you hit?

This program will give you the answer:

drawn = [5, 11, 9, 42, 3, 49]
bets = [3, 7, 11, 42, 34, 49]
hits = 0

for number in bets:
    if number in drawn:
        hits += 1

print(hits)

Note:

  • the drawn list stores all the drawn numbers;
  • the bets list stores your bets;
  • the hits variable counts your hits.

The program output is: 4.

X Tutup