I read through the Python 3.9 tutorial and wrote up these notes to help me remember certain things. Section headings and numberings follow those in the tutorial.

2 Using the interpreter

Arguments: import sys, then use sys.argv which is a list of strings. The 0th element tells you how the script was run, e.g. 'c' if it was python -c to run a command. The rest of the elements are the actual arguments.

4 More control flow tools

If d is a dict, d.items() is an iterable producing (key, value) pairs so that for k, v in d.items() is a handy way to iterate over key-value pairs.

In function calls, keyword arguments must come after any positional arguments. Definitions can specify which parameters are positional, positional-or-keyword, and keyword-only using forward slash and asterisk:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only

(diagram copied from the tutorial). Without the / and *, arguments are positional-or-keyword.

Scope: variable references check

  • the local symbol table, then
  • the local tables of any enclosing functions, then
  • the global table, then
  • the built-in names table.

The parameters to a function call are added to the local symbol table when it is called. The tutorial describes Python as “call by value” with the value being an object reference and then says that “call by object reference” would be a better name. Wiki refers to Python as call by sharing or call by object sharing. I think the best way to understand the behaviour is as follows: all modifications to the value of a passed variable are visible to the caller, but for immutable types we simply can’t do those modifications. If f(a) is called and a is an int, any assignment to a inside the body of f creates a new local called a and does not modify the caller’s a.

Docstrings are available with f.__doc__.

Type annotations work like this:

def f(a: str, b: int = 2) -> float
    return 0.0

5 Data structures

If l is a list, l.pop() pops the last element. You can also specify an index.

l.index(x) returns the index of the first x in l, a ValueError otherwise.

As of Python 3.8, you can do assignments in expressions but must use the “walrus” :=

l = [0, 1, 2]
while elt := l.pop():
    print(elt)

This is a syntax error in 3.7.

Recall that x or y has value either x or y. In short-circuited expressions, including longer ones, you get the last thing evaluated.

6 Modules

Inside a module, you can get the module name with __name__. This is particularly useful when you want to write code usable as a module and also as a standalone:

if __name__ == "__main__":
    # do stuff with sys.argv, ...

Inside the conditional you can put everything you want to happen if the file is run on its own. When people import your code, that stuff won’t be run, so they can just use whatever functions or objects you define.

The effect of from module import * can be controlled by creating a list of strings called __all__ in module.py.

7 IO

open(filename, mode) returns a file object. The mode should be 'w' for write only (erasing an existing file) 'a' for appending to an existing file, 'r' for read only, 'r+' for read and write.

8 Errors and exceptions

try can have an else clause which is run if there’s no exception. Putting code here, rather than in the try, avoids accidentally catching exceptions we didn’t mean to.

Exceptions can have values, called their argument. If you do

...
except Exception as inst:
    print(inst.args)

then args are the arguments to the exception, e.g. raise Exception('a', "b").

try has an optional finally clause which defines clean-up actions to be executed under all circumstances. It will execute as the last task before the try completes, whether or not the try raises an exception.

If you do with open(filename) as f: then the file object provides its own clean-up action (closing the file) which is automatically executed after the suite of the with. That’s why with is usually better in these circumstances.

9 Classes

There’s some particularly interesting stuff about scope and namespaces here. First of all, a namespace is a name-object mapping usually implemented with a dict. The reason this stuff is in the class section is that the name-object mapping corresponding to the attributes of an object makes up a namespace.

Scope

The discussion of scope was a little tricky to follow for me. Mathematicians tend to read definitions as the once-and-for-all final description of some concept or thing, to be taken completely literally. That’s not usually a productive way to read texts about coding or CS. For example, the first explanation of a scope in the tutorial is “a textual region of a Python program where a namespace is directly accessible.” (which namespace??) Directly accessible means that an unqualified reference to a name will cause Python to use that namespace to try and find the associated object. So a scope is associated to a namespace, and is a region of the program text. Presumably references to searching a scope mean looking things up in the namespace corresponding to that scope. The namespace referenced by a scope comes up a lot (e.g. “Outside functions, the local scope references the same namespace as the global scope”), presumably again this just refers to the namespace associated to the scope. But later we get things like “the global scope of a function defined in a module is that module’s namespace” (my emphasis), or “…a new namespace is created, and used as the global scope” which can’t really be taken literally.

Here’s an attempt to be pedantically correct. The scope of a name-object binding is the region of program text where that binding is valid (this is lexical scope). In general lots of name-object bindings will have the same scope, therefore we talk about the scope of a namespace as the common scope of each of its name-object bindings.

We consider each name-object binding to have its own identity beyond simply which name and which object it links. If we bind x to 2 in one part of our program and x to 2 in a completely separate part, we don’t want to be forced into considering both parts to belong to the scope of the x-to-2 binding.

There are various kinds of namespace - for example, the built-in namespace of built-in objects, the local namespace created each time a function is called, and the namespace for a module. Ordinarily global scope is the scope of the __main__ module namespace.

Python does name resolution by searching namespaces in the following order:

  • the local namespace. This is the local namespace for a class/function if we are in a class/function, or outside these, the module’s (global) namespace.
  • (if we are in a function or class) the local namespaces of any enclosing functions (classes)
  • the global namespace of the current module. If a function was defined in a module, its global namespace scope is that module, no matter where the function was called.
  • the namespace for built-in names, which is the global namespace for the builtin module.

The docs have a nice example which I will copy here, lightly modified.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def scope_test():
    def do_local():
        spam = "local spam"

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

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

    spam = "test spam"
    do_local()
    print(spam)
    do_nonlocal()
    print(spam)
    do_global()
    print(spam)

scope_test()
print(spam)

When scope_test() is called, line 13 binds spam to "test spam" in the local namespace for scope_test.

On line 14, do_local() is called. Its body has an assignment to spam, so spam is bound to "local spam" in the do_local local namespace. That doesn’t affect any other namespaces.

Each of the print calls on lines 14, 17, 19 look up spam in the local namespace of scope_test. Since we know do_local didn’t affect this namespace, the first print call prints test spam.

On line 16 do_nonlocal is called. The nonlocal keyword means that spam will be bound in the enclosing function scope_test’s local namespace. Therefore the local scope_test namespace now binds spam to "nonlocal spam". That’s what the line 17 print call will print.

On line 18 do_global is called which uses the global keyword. This binds spam in the global namespace and doesn’t change any other namespaces, so the succeeding print call prints "nonlocal spam" again.

Finally once we fall out of the function we have no local scope. The name spam is looked up in the global namespace where it refers to "global spam" because do_global created this binding.

9.3 A First Look at Classes

When a class definition is completed a class object is created, containing the local namespace for that class. Local scope reverts to being whatever it was before the class definition was entered, and a mapping for the class name is added to the local namespace.

Both class instances and classes themselves can have attributes. These are can be accessed with dot notation attribute references.

Class methods are not functions. Suppose a class C has a function f defined inside it like this:

class C:
    def f(self):
        return 0

and we then do c = C() so that c is an instance of C. Now C.f is a function, but the type of c.f is method. It isn’t a traditional function since we call it with c.f() whereas the function had a single parameter. In fact, c.f() is equivalent to C.f(c).

9.5 Inheritance

isinstance(object, class) and issubclass(c1, c2) can be useful.

9.6 Private variables

There’s no way in Python to restrict access to class attributes, but the convention is that names prefixed with _ should be treated as non-public.

10+11 A Brief Tour of the Standard Library

Some useful modules:

  • glob can make lists of files from wildcard searches
  • zlip does compression
  • doctest lets you embed tests in docstrings and run them
  • unittest does more sophisticated testing
  • reprlib helps display large/nested containers
  • string.Template has complex templating support
  • threading includes locks, events, semaphores
  • logging
  • bisect works efficiently with sorted lists
  • array has a homogeneous array type which is more space-efficient than lists
  • heapq does list-based heaps
  • decimal has a Decimal decimal float type, useful for financial applications (or exam mark rounding!). There are some good examples in the docs of how floating point and decimal arithmetic can differ.

12 Virtual environments and packages

The venv module handles these.

15 Floating point arithmetic

The actual stored value of a float is the nearest representable binary fraction: “on most machines today” a 53b numerator and a power-of-two denominator. You can see how your float x is represented with x.as_integer_ratio() which returns a numerator-denominator pair.

There’s a fractions module for exact rational arithmetic.