Python

Python is a neat programming language and fun to learn. It’s very popular, too; in fact, by almost all measures, it’s in the top five.

Overview

Python

As of October, 2021, the current production version is 3.10. Check out the list of versions, Wikipedia’s Python History and information about Python 2 versus Python 3.

NOTE: Python 2 and 3 are quite different. It is not generally possible to write code that works the same in both dialects. Python 2 is legacy, you should not be using it. While Python 3 was released in 2008 and Python 2 officially “died” on January 1, 2020, many people still use Python 2 and online answers to Python questions might use Python 2. You should not use Python 2. Please, just don’t.

These notes will only cover Python 3. If you need to learn Python 2, you have come to the wrong place.

Other excellent sources you will want to browse:

Getting Started

You can use Python in two ways:

  1. Write a program (script) in a text file and then pass it to the python interpreter, or
  2. Just enter the interactive mode and start typing.

These notes do not cover how to install Python on your system. Doing so can be tricky, especially since python2 is built-in to many systems 😱. In the notes below, you may have to replace the python command with python3.

Writing simple scripts

Our first script is a one-line file:

hello.py
print('Hello, world')

Run it like this:

$ python hello.py Hello, world

Another simple script:

triple.py
for c in range(1, 100):
    for b in range(1, c):
        for a in range(1, b):
            if a * a + b * b == c * c:
                print(f'({a},{b},{c})')

And another

fib.py
"""Writes Fibonacci numbers up to and including the first commandline argument."""

import sys

n = int(sys.argv[1])

a, b = 0, 1
while b <= n:
    print(b, end=' ')
    a, b = b, a+b
print()
$ python fib.py 300 1 1 2 3 5 8 13 21 34 55 89 144 233 $ python fib.py blah blah Traceback (most recent call last): File "fib.py", line 10, in ? n = int(sys.argv[1]) ValueError: invalid literal for int(): blah

Using the REPL

In the Python REPL, the prompt is >>>. The continuation line is .... For multiline inputs, enter a blank line to finish.

$ python
Python 3.10.0 (v3.10.0:b494f5935c, Oct  4 2021, 14:59:20)
[Clang 12.0.5 (clang-1205.0.22.11)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 5 * 3 + 6 / 1
21.0
>>> 5 / 2
2.5
>>> 5 // 2
2
>>> 5 < 9
True
>>> 'dog'
'dog'
>>> 5 if 1>2 else 3
3
>>> s = 'hello there'
>>> s.upper()
'HELLO THERE'
>>> s.split(' ')
['hello', 'there']
>>> {'x': 4, 'y': 6}
{'y': 6, 'x': 4}
>>> x = [1, True, 7.5, 'Hello', {'name':'sparky', 'breed':'shepherd'}, []]
>>> x[2]
7.5
>>> x[4]
{'name': 'sparky', 'breed': 'shepherd'}
>>> x[4]['name']
'sparky'
>>> x,y,z = 10,20,30
>>> x,y = y,x
>>> if x == 20:
...     print('dog')
...
dog
>>> def triple(x):
...     return x * 3
...
>>> triple(5)
15
>>> triple('ho')
'hohoho'
>>> x + y + 2j
(30+2j)
>>> (5-1j)*1j
(1+5j)
>>> triple(1j)
3j

Learning With Examples

Objects

Absolutely everything in Python is an object, and every object has an id:

>>> id(8)
4487363280
>>> id(20234235324555326437)
4490931176
>>> id(False)
4486979888
>>> id(2 < -4)
4486979888
>>> id(None)
4487080232
>>> id("hello")
4491034896
>>> id(id)
4488268752
That’s cool, right?

Python does not make that ridiculous distinction between primitives and references. Who needs that, anyway? Every expression produces an object!

Types

Every object has a type:

>>> type(8)
<class 'int'>
>>> type(2423542342356656575983745923479274923729399899898239443)
<class 'int'>
>>> type(False)
<class 'bool'>
>>> type(None)
<class 'NoneType'>
>>> type("Hello, how are you today?")
<class 'str'>
>>> type(2.77e6)
<class 'float'>
>>> type(3-3j)
<class 'complex'>
>>> type([4,5,6])
<class 'list'>
>>> type((9,4,'blue'))
<class 'tuple'>
>>> type({'CA':'Sacramento','HI':'Honolulu','AK':'Juneau'})
<class 'dict'>
>>> type(sum)
<class 'builtin_function_or_method'>
>>> type(lambda x: x * x)
<class 'function'>
>>> type(x for x in range(10))
<class 'generator'>
>>> int
<class 'int'>
>>> type(int)
<class 'type'>

Types are callable:

>>> str(42)
'42'
>>> bool(35443)
True
>>> bool(0)
False
>>> int(False)
0
>>> int(True)
1
>>> float(92)
92.0
>>> int(89.3221)
89
>>> int(99.99999999)
99
>>> int(-99.9999)
-99
>>> float(False)
0.0
>>> complex(5)
(5+0j)
>>> complex(3.6, 99)
(3.6+99j)
>>> int(32.4E39)
32399999999999999195056519073840702160896
>>> str(8.9E5)
'890000.0'
>>> int("32")
32
>>> list((3,7,5))
[3, 7, 5]
>>> tuple([1,2,-17,"dog"])
(1, 2, -17, 'dog')
>>> list("abc")
['a', 'b', 'c']
>>> list({"x": 5, "y": 7})
['x', 'y']
>>> str({"x": 5, "y": 7})
"{'x': 5, 'y': 7}"
Exercise: What does int("cafe") return? What about int("cafe", 16)? Why?
Truthy and Falsy

Things that are falsy: None, False, any numeric zero, any empty sequence, any empty mapping, instances of classes whose __nonzero__() or __len__() returns 0 or False

Sequences, Sets, and Mappings

Python has a bunch of cool compound types built-in:

TypeDescription
strimmutable, ordered sequence of values representing Unicode code points
bytesimmutable, ordered sequence of 8-bit bytes (in the range 0..255)
tupleimmutable, ordered sequence of arbitrary objects
listmutable, ordered sequence of arbitrary objects
bytearraymutable, ordered sequence of 8-bit bytes (in the range 0..255)
frozensetimmutable, unordered collection of arbitrary objects
setmutable, unordered collection of arbitrary objects
dictmutable dictionary (but the keys must be immutable)

For all sequences s and t, sequence elements x, and integers i, j, k, and n, you can write:

    len(s)
    s[i]       s[i:j]     s[i:j:k]    s[:i]      s[i:]
    x in s     x not in s
    s + t      s * n
    s < t      s <= t     s >= t      s > t
    iter(x)

Examples:

>>> a = [10,20,30,40,50,60,70,80,90,100]
>>> a[3]
40
>>> a[3:5]
[40, 50]
>>> a[8:]
[90, 100]
>>> a[:2]
[10, 20]
>>> a[1:7:3]
[20, 50]
>>> 38 in a
False
>>> 98 not in a
True
>>> len(a)
10
>>> b = [16,32,64]
>>> a + b
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 16, 32, 64]
>>> a < b
True
>>> a == b
False
>>> c = [16,32,64]
>>> b == c
True
>>> b is c
False
>>> c * 5
[16, 32, 64, 16, 32, 64, 16, 32, 64, 16, 32, 64, 16, 32, 64]

See the Python reference for details on the comparison operators.

In case you haven’t heard the term immutable before:

>>> point = (3, 5)
>>> point[0]
3
>>> point[1] = 3
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'tuple' object does not support item assignment

You can unpack (what JavaScript calls “spreads” and Ruby calls “splats”) sets, sequences, and mappings:

>>> a = [10, 5, 13]
>>> [3, *a, 21]
[3, 10, 5, 13, 21]
>>> primaries = {'red': 'rojo', 'green': 'verde', 'blue': 'azul'}
>>> secondaries = {'cyan': 'cian', 'magenta': 'magenta', 'yellow': 'amarillo'}
>>> {'black': 'negro', **primaries, **secondaries, 'white': 'blanco'}
{'black': 'negro', 'red': 'rojo', 'green': 'verde', 'blue': 'azul', 'cyan': 'cian', 
'magenta': 'magenta', 'yellow': 'amarillo', 'white': 'blanco'}
Exercise: But can you write x, y, z = *a?

Use sets, sequences, and mappings in for-statements:

friends = ['Eren', 'Mikasa', 'Armin']
for friend in friends:
    print(friend.upper())

capitals = {'Colima': 'Pachuca', 'Nayarit': 'Tepic', 'Yucatán': 'Mérida'}
for state, capital in capitals.items():
     print(f'The capital of {state} is {capital}')

Program Structure

Structure is defined by indentation: there are no braces, no line ending semicolons, no begin or end symbols.

Functions

Functions are introduced with def, and called with the usual f(x) notation. It is common, but not required, for the first statement in the function body to be a plain string to serve as the function’s documentation.

stats.py
"""This script that displays the mean and median of
an array of values, passed in on the command line.
"""

import sys
import operator


def median(a):
    """Return the median of sequence a"""
    a = sorted(a)
    length = len(a)
    if length % 2 == 1:
        return a[length // 2]
    else:
        return (a[length // 2] + a[length // 2 - 1]) / 2


def mean(a):
    """Return the mean of the values in sequence a"""
    return sum(a) / len(a)


print("input array is:", sys.argv)
numbers = [int(x) for x in sys.argv[1:]]
print("list is:", numbers)
print("sum is:", sum(numbers))
print("mean is:", mean(numbers))
print("median is:", median(numbers))
print("list is:", numbers, "(just making sure it was not changed)")
print(mean.__doc__)
Exercise: Find out how, given a function, you can extract its documenatation string.

Positional and Keyword Arguments

When calling functions, your arguments are either positional arguments or keyword arguments (a.k.a. kwargs). Keyword arguments are those prefixed with their parameter name. This is freaking awesome.

>>> def f(x, y, z):
...     return (x, y, z)
...
>>> f(x=1, y=8, z=20)
(1, 8, 20)
>>> f(z=1, y=5, x=10)
(10, 5, 1)
>>> f(20, z=9, y=7)
(20, 7, 9)
>>> f(x=1, 2, 3)
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
>>> def line(x1, y1, x2, y2, color, thickness, style):
...    pass
...
>>> line(color="red", thickness=1, style="dashed", x2=9, x1=4, y1=10, y2=8)

It’s not just the flexibility of being able to specify the arguments in any order that is the most awesome; it’s just so incredibly readable. You don’t have to look elsewhere to see what the call means.

Exercise: Read Bret Victor’s Learnable Programming. What principle of learnable programming is facilitated by Python’s keyword arguments?

Positional-only and Keyword-only Arguments

You can force callers to supply certain arguments positionally force them to supply certain arguments as keyword arguments. By default, callers can do either. Here’s the example from the Python docs:

def f(a, b, /, c, d, *, e, f):
    print(a, b, c, d, e, f)

f(10, 20, 30, d=40, e=50, f=60)

Here arguments for a and b must be positional; e and f must be keyword; c and d can be either.

Exercise: What can you say about a definition such as def fun(*, param1, param2, param3)? What can you say about a definition such as def fun(param1, param2, param3, /)?
Exercise: Why would you ever use the /? Does it buy you anything?

Default Arguments

You can specify values that a parameter will take if no matching argument is supplied.

>>> def f(x, y=5, z=10):
...     return (x, y, z)
...
>>> f(1)
(1, 5, 10)
>>> f(1, 7)
(1, 7, 10)
>>> f(1, 8, 20)
(1, 8, 20)
>>> f(2, 3, 5, 7)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: f() takes from 1 to 3 positional arguments but 4 were given
>>> f(2, z=5)
(2, 5, 5)
>>> f(x=4, y=9)
(4, 9, 10)
>>> f(z=1, x=10, y=3)
(10, 3, 1)
>>> def line(x1, y1, x2, y2, color="black", thickness=1, style="solid"):
...    pass

You can roll up excess arguments!

You can have calls with an unlimited number of arguments. The single starred parameter collects the excess positional arguments into a tuple.

>>> def g(x, *y):
...     return f'x is {x} and y is {y}'
...
>>> g(4)
'x is 4 and y is ()'
>>> g(3, 4)
'x is 3 and y is (4,)'
>>> g(8,3,4,5,6,2,3)
'x is 8 and y is (3, 4, 5, 6, 2, 3)'

The double-starred argument will become a dictionary of all excess keyword arguments:

>>> def g(x, **y):
...     return f'x is {x} and y is {y}'
...
>>> g(10)
'x is 10 and y is {}'
>>> g(1, a=3, b=5)
"x is 1 and y is {'a': 3, 'b': 5}"
>>> g(p='hello', x=8)
"x is 8 and y is {'p': 'hello'}"
Exercise: What is g(1, x=5)? Why?

Here’s what the docs say:

If the form *identifier is present, it is initialized to a tuple receiving any excess positional parameters, defaulting to the empty tuple. If the form **identifier is present, it is initialized to a new ordered mapping receiving any excess keyword arguments, defaulting to a new empty mapping of the same type. Parameters after * or *identifier are keyword-only parameters and may only be passed used keyword arguments.
Exercise: Explain in English the intent of each of these functions, and create sample invocations for each:
  • def f1(x, *y, z): print(x, y, z)
  • def f2(x, *y, **z): print(x, y, z)
  • def f3(x, *, y, z): print(x, y, z)
  • def f4(*, x, y, z): print(x, y, z)
  • def f5(*x, **y): print(x, y)
  • def f6(*x, y, **z): print(x, y, z)

Higher-order Functions

Functions are objects too. They can be stored in variables. They can be passed as arguments and returned from other functions.

hof.py
"""Examples of higher order functions in Python."""

# First, some plain old NAMED functions
def is_odd(x): return x % 2 == 1
def two_x_plus_y(x, y): return 2 * x + y
def square(x): return x * x

# We can also use lambdas (UNNAMED functions)
addSix = lambda x: x + 6

# Functions can accept functions as parameters and even return functions
def compose(f, g):
    return lambda x: f(g(x))

def twice(f):
    return compose(f, f)

# Let's make some calls
add_six_then_square = compose(square, addSix)
add_twelve = twice(addSix)
assert add_six_then_square(9) == 225
assert twice(square)(3) == 81
assert add_twelve(100) == 112

Note: lambda may look cool, and it is very common in Haskell, Lisp, Scheme, Clojure, and JavaScript, but it is not common in Python. You’ll generally see list comprehensions and explicit for-loops.

Scope

Python functions have their own local variables (including parameters), just like most lexically scoped languages. You can read non-locals but writing to them requires global or nonlocal.

scopedemo.py
dog = "Sparky"
rat = "Oreo"
def f():
    dog = "Rex"                          # This makes a new local variable
    global rat
    rat = "Cinnamon"                     # This assigns to the global variable
    print("In function:", (dog, rat))    # Uses locals when available, else looks outside

print("Outside function:", (dog, rat))
f()
print("Outside function:", (dog, rat))

Closures

A closure is an inner function that retains references to variables in an enclosing function, even after the enclosing function has gone. Simple example:

closure_demo.py
def sequence(start, delta):
    value = start
    def advance():
        nonlocal value
        current = value
        value += delta
        return current
    return advance

s = sequence(start=10, delta=3)

assert(s() == 10)
assert(s() == 13)
assert(s() == 16)

Comprehensions

A list comprehension just a list written with an expression or expressions used to generate the list elements. They are generally used instead of map and filter.

>>> a = [1, 2, 10, 6]
>>> b = [3, 0, 4, -8]
>>> [4 * x for x in a]
[4, 8, 40, 24]
>>> [2 / x + 5 for x in a if x < 4]
[7.0, 6.0]
>>> [x + y for x in a for y in b]
[4, 1, 5, -7, 5, 2, 6, -6, 13, 10, 14, 2, 9, 6, 10, -2]
>>> [(x, x**3) for x in a]
[(1, 1), (2, 8), (10, 1000), (6, 216)]
>>> [x + y for x in a for y in b if y <= 0]
[1, -7, 2, -6, 10, 2, 6, -2]
>>> [a[i] * b[i] for i in range(len(a))]
[3, 0, 40, -48]
>>> [str(round(355/113.0, i)) for i in range(1,6)]
['3.1', '3.14', '3.142', '3.1416', '3.14159']
>>> [(i, 2**i) for i in range(10)]
[(0, 1), (1, 2), (2, 4), (3, 8), (4, 16), (5, 32), (6, 64), (7, 128), (8, 256), (9, 512)]
>>> [x for x in [y*y for y in range(10)] if x % 2 == 0]
[0, 4, 16, 36, 64]
>>> names = ["ALICE", "BOb", "cAROl", "daVE"]
>>> [(n.lower(), n.upper()) for n in names]
[('alice', 'ALICE'), ('bob', 'BOB'), ('carol', 'CAROL'), ('dave', 'DAVE')]
>>> [i for i in range(10,30) if i not in range(5, 40, 2)]
[10, 12, 14, 16, 18, 20, 22, 24, 26, 28]
>>> [(a,b,c) for c in range(1,30) for b in range(1,c) \
      for a in range(1,b) if a*a+b*b==c*c]
[(3, 4, 5), (6, 8, 10), (5, 12, 13), (9, 12, 15), (8, 15, 17),
(12, 16, 20), (15, 20, 25), (7, 24, 25), (10, 24, 26), (20, 21, 29)]

Python list comprehensions are:

Python has set comprehensions and dictionary comprehensions also.

>>> {x for x in range(16) if x % 3 == 0}
{0, 3, 6, 9, 12, 15}
>>> {k:2**k for k in range(5)} 
{0: 1, 1: 2, 2: 4, 3: 8, 4: 16}

Type Hints

By the way, the syntax of Python allows you to mark up your code with typing information. Such markup does not in any way affect the way your code runs. In fact, at run time all of this markup is completely ignored. It only matters for text editors, IDEs, and other tools that can look at your code. Here are some gratuitous examples:

>>> dozen: int = 12
>>> found: bool
>>> found = False
>>> from typing import List, Set, Dict, Tuple
>>> def first_two(a: List[float]) -> Tuple[float, float]:
...     return (a[0], a[1])
... 
>>> first_two([False, 0, 17, 2.2, []])
(False, 0)

Statements

Most of the Python statements are pretty common and easy to understand, but there are a few things that are pretty cool:

The else clause on a for-loop (runs only when the loop finishes normally, NOT when it breaks or returns):

for place in places:
    if good(place):
        print(f"Ah, a good place: {place}")
        break
else:
    print("No good places for you")

The else clause on a while-loop (runs only when the loop finishes normally, NOT when it breaks or returns):

tries, guess = 0, None
while guess != answer:
    guess = input("What is your guess? ")
    if malformed(guess):
        print("You are not playing fair, game over")
        break
else:
    print(f"You got it, it was: {guess}")

Pattern Matching. The match statement is pretty flexible, allowing structural cases common in a few other languages. Cases can be simple values, tuple patterns, list patterns, constructor patterns, and can be made of other patterns separated with |. Patterns can also have guards. (Tutorial) (Reference)

match e:
    case 2 | 3 | 5 | 7 | 11:
        print("Small prime")
    case int(n) if n % 5 == 0:
        print("Multiple of 5")
    case (_, x, _):
        print(f"A three-tuple with {x} in the middle")
    case {'id': y}:
        # Matches any dictionary that has an id key, binds value to y
        print(f"Your identifier is {y}")
    case _:
        print("What?")

The with statement makes working with context managers awesome:

with open("important_message.txt", "w") as file:
    file.write("Hello, World!")

Modules

Modules are trivial in Python: no special syntax is required: the module name is just the file name. For example, put this code in roman.py:

roman.py
"""A module containing a single subroutine that returns the roman
numeral representation of an integer."""

def to_roman(n):
    """Returns a string representing the roman numeral for n"""

    if n <= 0 or n >= 4000:
        raise ValueError(f'{n} is too big or has no Roman equivalent')

    map = [
        (1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'), (100, 'C'), (90, 'XC'),
        (50, 'L'), (40, 'XL'), (10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I')]

    result = []
    for value, name in map:
        while (value <= n):
            result.append(name)
            n -= value
    return ''.join(result)

if __name__ == "__main__":
    n = int(input('Enter an integer: '))
    print(f'{n} is {to_roman(n)}')
    print('And here are other interesting roman numerals')
    for n in [3, 89, 22, 11]:
        print(f'{n} is {to_roman(n)}')

Note the if __name__ == "__main__": that guards all the script-level code. This allows the module to be run as a top-level script if you like, since when you run a script from the commandline, __name__ will be "__main__", but when imported, __name__ will be the name of the module.

Read more about this important Python idiom at StackOverflow.

Here is a script that uses it:

romanfilter.py
"""A simple script that reads from integers from standard
input and writes their roman equivalents to standard output.
The input file is assumed to have one integer per line.
Blank lines are allowed and will be skipped.
"""

import roman, sys

for line in sys.stdin:
    line = line.strip()
    if line:
        try:
            print(f'{line} is {roman.to_roman(int(line))}')
        except Exception as e:
            print(f'{type(e).__name__}: {e}')

Classes

The Basics

You can probably figure this out by example. By the way, in Python we say that instances of classes have attributes (rather than “fields,” “slots,” “members,” or “variables”).

circle.py
import math

class Circle:
    "A circle with a 2-D center point and a radius."

    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius

    def area(self):
        "Returns the area of the circle"
        return math.pi * (self.radius ** 2)

    def perimeter(self):
        "Returns the circumference of the circle"
        return math.pi * self.radius * 2

    def expand(self, factor):
        "Increases the radius by the given factor"
        self.radius *= factor
        return self

    def move(self, dx, dy):
        "Moves the center point by <dx, dy>"
        self.x += dx
        self.y += dy
        return self

    def __repr__(self):
        return f'Circle at ({self.x},{self.y}) with r={self.radius}'

Using the class:

>>> from circle import Circle
>>> c = Circle(4, 3, 10)
>>> c
Circle at (4,3) with r=10
>>> c.area()
314.1592653589793
>>> c.perimeter()
62.83185307179586
>>> c.move(3,2)
Circle at (7,5) with r=10
>>> c.move(-3,2).expand(5)
Circle at (4,7) with r=50
>>> Circle.__doc__
'A circle with a 2-D center point and a radius.'
>>> Circle.area.__doc__
'Returns the area of the circle'
>>> >>> Circle.area(c)
7853.981633974483
>>> >>> c.area()
7853.981633974483
Exercise: Try help(Circle).

The important concepts illustrated by this example are:

Inheritance and Polymorphism

Nothing too unusual here:

animals.py
"""A module with talking animals."""

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f'{self.name} says {self.sound()}')

class Cow(Animal):
    def __init__(self, name):
        super().__init__(name)

    def sound(self):
        return 'moo'

class Horse(Animal):
    def __init__(self, name):
        super().__init__(name)

    def sound(self):
        return 'neigh'

class Sheep(Animal):
    def __init__(self, name):
        super().__init__(name)

    def sound(self):
        return 'baaaaa'

if __name__ == '__main__':
    s = Horse('CJ')
    s.speak()
    c = Cow('Bessie')
    c.speak()
    Sheep('Little Lamb').speak()

Class attributes

You can create an attribute of the class object itself.

>>> class Dog:
...     family = 'canine'
...     def __init__(self, n):
...         self.name = n
...
>>> a = Dog('Sparky')
>>> b = Dog('Spike')
>>> (a.name, b.name, a.family, b.family, Dog.family)
('Sparky', 'Spike', 'canine', 'canine', 'canine')
>>> Dog.family = 'Canine'
>>> (a.name, b.name, a.family, b.family, Dog.family)
('Sparky', 'Spike', 'Canine', 'Canine', 'Canine')
Exercise: What would have happened if instead of changing Dog.family, we changed a.family instead? Why?

Operators

Operators are really methods of a class that get a nice syntax via rewriting rules.

>>> x = 3
>>> x.__add__(5)
8

See the Operators section in the Python Reference Manual for the complete list.

The walrus operator is not in this list, but you should know it. It’s really just the assignment operator that JavaScript, Java, and the C-family languages all have. Here’s the deal in Python though:

Compare:

while True:
    command = input('> ')
    if current == 'Q':
        break
    execute(command)
while (command := input('> ')) != 'Q':
    execute(command)




Use it only when it helps; the addition of this operator in Python 3.8 is said to have been the straw that broke the camel’s back that got Guido to step down as BDFL. “Now that PEP 572 is done, I don't ever want to have to fight so hard for a PEP and find that so many people despise my decisions. I would like to remove myself entirely from the decision process.”

Here are examples from the proposal that was written to add the operator to the language:

# Handle a matched regex
if (match := pattern.search(data)) is not None:
    # Do something with match

# A loop that can't be trivially rewritten using 2-arg iter()
while chunk := file.read(8192):
    process(chunk)

# Reuse a value that's expensive to compute
[y := f(x), y**2, y**3]

# Share a subexpression between a comprehension filter clause and its output
filtered_data = [y for x in data if (y := f(x)) is not None]
Exercise: Read PEP 572. What do you think? Discuss with friends.

Iterators

An iterator is an on-demand, or lazy, sequence—values are generated only when needed. Iterators are space-efficient and allow for inifinite seqeunces.

for line in file.readlines():   # readlines produces a list, not good for large files
    # ...

with

for line in file:               # better to read each line as we go!
    # ...

An iterator is any object with two methods:

Any object that has an __iter__() method that returns an iterator can be used in a for statement.

iteratordemos.py
"""A couple of interesting iterators."""

class fibs:
    """Fibonacci iterator (infinite): 0, 1, 1, 2, 3, 5, 8, ..."""

    def __init__(self):
        self.a, self.b = 0, 1

    def __next__(self):
        current, self.a, self.b = self.a, self.b, self.a + self.b
        return current

    def __iter__(self):
        return self

class collatz:
    """Collatz sequence iterator, supply start value at initialization"""

    def __init__(self, start):
        self.n = start
        self.started = False

    def __next__(self):
        if not self.started:
            self.started = True
            return self.n
        elif self.n == 1:
            raise StopIteration
        elif self.n % 2 == 0:
            self.n = self.n // 2
        else:
            self.n = 3 * self.n + 1
        return self.n

    def __iter__(self):
        return self

if __name__ == "__main__":

    print('The first few Fibonacci numbers:')
    for i in fibs():
        print(i, end=' ')
        if i > 1000000000:
            break
    print()
    print()

    print('A collatz sequence:')
    for i in collatz(47):
        print(i, end=' ')
    print()
    print()

Generators

In practice, you don’t always write iterators yourself. Instead, you make generators, which make the iterators for you.

generatordemos.py
"""A couple of interesting generators."""

def fibs():
    "Fibonacci generator (infinite): 0, 1, 1, 2, 3, 5, 8, ..."
    a, b = 0, 1
    while True:
        current, a, b = a, b, a + b
        yield current

def collatz(n):
    "Collatz generator starting at n"
    while True:
        yield n
        if n == 1:
            return
        elif n % 2 == 0:
            n = n // 2
        else:
            n = 3 * n + 1

if __name__ == "__main__":

    print('The first few Fibonacci numbers:')
    for i in fibs():
        print(i, end=' ')
        if i > 1000000000:
            break
    print()
    print()

    print('A collatz sequence:')
    for i in collatz(47):
        print(i, end=' ')
    print()
    print()

If the generator is only going to be used once, you can use a generator expression, which looks like a list comprehension except it uses parentheses rather than brackets. Here are two cool examples from the Python documentation:

sum_of_squares = sum(i*i for i in range(10))
unique_words = set(word for line in page for word in line.split())

Decorators

A decorator is a callable that takes a function as an argument and returns, usually, another function (though technically it can return anything). They are generally invoked with the @ syntax which replaces the name of a function with the decorated version. Example:

decoratordemo.py
def traced(f):
    def log_then_call(*args, **kwargs):
        print('Calling', f.__name__)
        f(*args, **kwargs)
    return log_then_call

@traced
def greet(name):
    print('Hello,', name)

greet('Alice')

# The decorator syntax is just shorthand for greet = traced(greet)

Probably the two most common decorators you will encounter are @property and @classmethod:

rectangle.py
class Rectangle(object):
    def __init__(self, w, h):
        self.width = w
        self.height = h

    @property
    def area(self):
        return self.width * self.height

    @classmethod
    def from_string(cls, description):
        size = [int(dimension.strip()) for dimension in description.split(',')]
        return Rectangle(size[0], size[1])

if __name__ == '__main__':
    r = Rectangle.from_string(' 7 ,6')
    print(r.area)

When writing webapps, it’s common to create decorators like, say, @authenticated, so that functions that happen when a web request is received will automatically check whether there is a logged-in user. So much better than dropping if-statements into all those functions!

By the way, here is a nice intro to decorators.

Reference Material

Keywords

Keywords cannot be used as ordinary identifiers.

The words match and case are called soft keywords because they can be used as identifiers, except in match statements.

Grammar

Check out the Full Grammar Specification!

Types

From the Reference Manual section on the standard type hiearchy:

TypeNotes
NoneType contains only one value: None
NotImplementedTypecontains only one value: NotImplemented
ellipsis contains only one value: Ellipsis
Number all numbers are immutable
Integer integers
int range limited only by available memory
bool contains only two values: True, False
float hardware’s double-precision type
complex pair of hardware doubles (z.real, z.imag)
Sequence finite, ordered, indexed from 0. Supports len(), a[i], a[i:j], a[i:j:k]
Immutable Sequence a sequence that cannot have its value changed
str contain code units
bytes contain bytes
tuple written as (), (x,), (x,y), (x,y,z), etc.
Mutable Sequence subscripted and slice forms can be lvalues; del statement okay
list written as [], [x], [x,y], [x,y,z], etc.
bytearray contain bytes
Set unordered, finite collection of unique, immutable objects
set mutable
frozenset immutable
Mapping something indexable with the a[k] syntax, k can be almost anything, not just an integer
dict the only kind of mapping type
Callable anything supporting the call syntax
function created by a function definition (user-defined function)
method basically a function defined within a class
generator any function with a yield statement
Coroutine Function a function defined with async def
Async Generator
Function
a function defined with async def and has a yield statement
Built-in Function wrapper around a C function (e.g. len(), math.sin())
Built-in Method wrapper around a C function, w/ implicit __self__ argument
Class  a class.
Class Instance any instance of a class with a __call__() method
module a collection of definitions and statements put into a file and imported into other code
Custom class created by a class definition
Class instance an object created by invoking a constructor
I/O an open file, created with open() and related operations. sys.stdin and sys.stdout are examples.
Internal used internally, but exposed to the programmer
Code bytecode. Immutable.
Frame execution frames, occurring in traceback objects
Traceback created when an exception is thrown, accessible within the tuple returned by sys.exc_info()
Slice examples: a[i:j:step], a[i:j, k:n], a[..., i:j]
Static Method created by built-in staticmethod() constructor
Class Method created by built-in classmethod() constructor

Statements

Python is not a simple language, really; there are at least 20 kinds of statements!

__builtins__

These identifiers are always available. You can see these identifiers with:

>>> ' '.join(sorted(k for k in __builtins__.__dict__))

Here is the list of module contents for Python 3.9.7:

Standard Modules

There are a few hundred standard modules in the Python Library.

Python in Practice

You want to get really good at Python! You want to write pythonic code. You want to be a Pythonista. How can you do this? A little advice:

When you start writing large-scale Python applications, you will need to, among other things:

Python has made it big in the world of Data Science and other fields, because of some libraries you’ll probably wish to learn some day:

Here’s a nice tutorial with NumPy, SciPy, and Matplotlob.