Python Course

Session 3



Table of Contents


More on strings



Name scope

Input and Output


File as a Database



The sessions now broadly follow the standard tutorial, but in a very condensed way. You are very much advised to refer to the tutorial for a fuller coverage of the language; and indeed, to the full language reference for complete coverage. Some of the examples shown here are taken from the tutorial.

This session adds to what was learnt from session 2, Syntax and Constructs, and goes on to cover some further language constructs.

Please feel free to seek assistance with understanding Python, particularly with the topics covered in the course sessions.



In the notes for this session, where the description splits into two columns, the right column shows examples. E.g.:


a + b * (c + d)


Where the description splits into three columns, the middle column shows examples and the right column shows results of evaluation of the examples. E.g.:

Expression evaluation

2 + 3 / (4 + 6)



2 ** 9



More on strings

Strings are a fundamental aspect of programming languages and here we cover them in more detail.

Strings are immutable: you can't modify a string object.  So you have to create a new string object if you want a modified version of an existing string.


"abc" * 3

"abc" * "abc"




"abc" "abc"

"abc" + "abc"

s = "xyz" ; "abc" + s

s = "xyz" ; "abc" s





Negative indexes
















Any object as a string






A string is an object, and has (a lot of)  methods (functions) that can be applied to a string. It's useful to know them! (BTW there is no built-in function to replace a slice of a string.)

Note: str.format() in particular provides useful formatting mechanisms for strings. If you're used to C then Python also supports printf style string formatting (but str.format is preferred).


A string is a sequence type and so is a list, so there are operations which act equivalently on them. So the table above also works replacing the strings with lists. Except that you can't concatenate by having adjacent lists.

Unlike strings, lists are mutable. So whereas you cannot modify a string, you can modify a list, e.g.:

cubes = [1, 8, 27, 65, 125]  # A list of cubes.

cubes[3] = 64                # Get it right!

cubes.append(6**3)           # Add another cube.

cubes.extend([7**3, 8**3])   # Add a couple more cubes.


None of the three list modifications are allowed for strings. Be aware that if you modify an object then all references to that object will, of course, see that change.


Example loop generating prime numbers. Shows nested loops, break statement and else part. The else part is executed if the break isn't taken, i.e. when the loop completes all its iterations.

for n in range(2, 10):

    for x in range(2, n):

        if n % x == 0:

            print(n, 'equals', x, '*', n//x)


    else:  # No "break" occurred.

        # loop fell through without finding a factor

        print(n, 'is a prime number')




2 is a prime number

3 is a prime number

4 equals 2 * 2

5 is a prime number

6 equals 2 * 3

7 is a prime number

8 equals 2 * 4

9 equals 3 * 3


The general form of the range() function is: range(start, end, increment)


A continue statement can be used to skip to the next iteration of the loop, e.g.:

for num in range(2, 10):

    if num % 2 == 0:

        print("Found an even number", num)


    print("Found a number", num)



A function is a fundamental concept in programming languages, borrowed from mathematics, e.g. the sine function “sin(angle)”. It allows an algorithm to be encapsulated so that it can be executed from different places in a program. A function can have parameters which are passed when it is invoked, e.g. a parameter specifying an angle value. A function can contain any amount of code. A python function always returns a value (which is None if there is no explicit return statement).

def fib(n):

    """Return a list containing the Fibonacci series up to n."""

    result = []  # An empty list

    a, b = 0, 1

    while a < n:

        result.append(a)  # Append to the list

        a, b = b, a+b

    return result  # Return the list


# Now call the function we just defined:

print(fib(2000))    # Call it and print the list returned.

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597]


Or, use the * expansion operator to expand out (“unpack”) the list:

print(fib(2000))    # Call it and print the contents of the list returned.

0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597



def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):

    while True:

        ok = input(prompt)

        if ok in ('y', 'ye', 'yes'):

            return True

        if ok in ('n', 'no', 'nop', 'nope'):

            return False

        retries = retries - 1

        if retries < 0:

            raise OSError('uncooperative user')



This function can be called in several ways:

ask_ok('Really quit?', 2)  # Rely on default for last parameter.

ask_ok('OK to overwrite the file?', complaint='Come on, only yes or no!')

    # Rely on default for last parameter.

ask_ok()  # Error because "prompt" has no value.


A typical, and good layout for a Python module has essentially all the code contained within functions, even resulting in some functions that are only called once. It helps to encapsulate distinct functionality and clarify the operation of the overall module.


Name scope

The interpreter executes code as it goes along. It builds many name tables (symbol tables), remembering names as they are introduced and associating those names with the objects to which they refer. Then when a name is referenced, if it isn't yet defined then it's an error; otherwise it points to the object and the interpreter is able to use that to continue execution appropriately. Names can be local to an area of code, with no visibility outside that. That usefully allows separate functions to define and use variables locally within their bounds without clashing with other functions which may have coincidentally used the same named variable for a different purpose. E.g.:

def sum(a, b):

    return a + b


def sumandsquare(a, b):

     return sum(a, b) * sum(a, b)


a = 23

b = 27

print(sumandsquare(a, b))



The names a and b don't clash between functions, or the outer: the execution of a function introduces a new symbol table used for the local variables of that function. In the example, there will be three distinct symbols tables containing the names a and b, all identifying different objects.

Input and Output

A disk file consists simply of a sequence of bytes. The file is automatically extended as you write more bytes.

You can input data (text or bytes) from a file or from stdin (the text window you are running your program in).

You can output data (text or bytes) to a file or to stdout (the text window you are running your program in).

You have to open a file for reading, or for writing, before you read or write data from or t that file. And you have to close it afterwards.

There are general functions available to format the data you write, and to help interpret the data you read.

You can do line-oriented reading/writing, or just read/write text/bytes without regard for a line structure.

There are facilities for writing and reading Python objects to a dedicated file, using json. Those facilities serialise and dedeserialise the objects. Serialisation records the object data, and its structure, in a way that deserialisation can reconstruct the original object.

Operations on files should always be checked for success, and failures handled and reported appropriately. File operations can so easily fail – e.g. you don't have permission to read/write the file, or you've run out of disk space.


Create a file called Fred in the current directory. The file is opened for writing. If the file already exists, it will simply be overwritten. The open call returns a “file object” :-

fout = open('Fred', 'w')  # 'w' opens it for writing.


Write some text to the file:

fout.write('This is some text\n')

fout.write('with a number: ' + str(42) + '\n')


Close the file:



Open the file for reading; read the data; and close the file:

fin = open('Fred')  # Second parameter defaults to 'r'.

fredsdata =  # Read all the data.



You can instead read successive lines from a file using the readline() method. When readline() ultimately returns an empty string, that indicates you've reached the end of the file. E.g.:

while 1:

    line = fin.readline()

    if line == '':  break

    print(line, end='')


Or, nicely, you can regard the file object as an iterator and do:

for line in fin:

    print(line, end='')

File as a Database

You can use a file as a database, updating parts of it:

# Create an empty file called dbs, failing if it already exists:

fdbs = open('dbs', 'x')  # 'x' means exclusive creation.



fdbs = open('dbs', 'r+b')  # binary (bytes) read/write.  # Position at start of file.

fdbs.write(b'just sum text')  # Position after the first 5 bytes

fdbs.write(b'some text')



And that outputs:

b'just some text'


Note that print and write are fairly similar in what they achieve. And print has a file argument so you can write e.g.:

print (b'some text', file=fdbs)


But print doesn't print data as bytes: it prints them as Unicode characters. So it doesn't work for writing to a binary file.