Advanced python CoP

Table of contents:

  • Iter protocl
  • Generators
  • Contextlib

Iter protocol

In [1]:
%%HTML
<iframe src='https://gfycat.com/ifr/YearlyWelcomeBlowfish' frameborder='0' scrolling='no' allowfullscreen width='640' height='1185'></iframe>
In [1]:
for value in [1, 2, 3]:
    print(value)
1
2
3
In [1]:
class Counter:
    pass
In [2]:
for value in Counter():
    print(value)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-195ccddbe93a> in <module>
----> 1 for value in Counter():
      2     print(value)

TypeError: 'Counter' object is not iterable

For loop under the hood

In [3]:
it = iter([1, 2, 3])
try:
    while True:
        value = next(it)
        print(value)
except StopIteration:
    pass
1
2
3

How to create iterable object? Implement __iter__ that returns object implementing __next__

In [28]:
class Counter:
    def __init__(self, low, high):
        self.current = low
        self.high = high

    def __iter__(self):
        return self

    def __next__(self):
        if self.current > self.high:
            raise StopIteration
        else:
            self.current += 1
            return self.current - 1


for i in Counter(3, 8):
    print(i)
3
4
5
6
7
8

You don't have to return self in __iter__

In [7]:
class Counter:
    def __iter__(self):
        return iter([1, 2, 3])

for value in Counter():
    print(value)
1
2
3

Why is this information useful? Infinite or very large sequences.
Problem: find first fibonaci sequence number which has a sum of digits greater than 100

In [11]:
from itertools import islice


class FibonacciIterator:
    def __iter__(self):
        self.a = 0
        self.b = 1
        return self

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


list(islice(FibonacciIterator(), 10))
Out[11]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
In [15]:
from typing import Iterable, Callable, Any

sum_digits = lambda n: sum(map(int, str(n)))

def find_first(iterable: Iterable, predicate: Callable[[Any], bool]):
    for x in iterable:
        if predicate(x):
            return x

find_first(FibonacciIterator(), lambda x: sum_digits(x) > 100)
# next(x for x in FibonaciIterator() if sum_digits(x) > 100)
Out[15]:
218922995834555169026

Generators

Generator is just like a container, but values are generated on the fly as you iterate.

In [16]:
generator = range(10000000)
big_list = list(generator)

from sys import getsizeof

print(getsizeof(generator))
print(getsizeof(big_list))
48
80000056

Generator comprehensions:

In [17]:
%%timeit
power_2 = [i**2 for i in range(10**6)]
292 ms ± 10 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [19]:
%%timeit
power_2_gen = (i**2 for i in range(10**6))
533 ns ± 13.6 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

It's waay faster because no i**2 was computed :P. We only created a recipe for a sequence.

In [20]:
! pip3.8 install memory_profiler --user
Requirement already satisfied: memory_profiler in /home/gjklv8/.local/lib/python3.8/site-packages (0.55.0)
Requirement already satisfied: psutil in /usr/local/lib/python3.8/dist-packages (from memory_profiler) (5.6.3)
In [21]:
%load_ext memory_profiler
In [22]:
%memit sum([i**2 for i in range(10**6)])
peak memory: 488.84 MiB, increment: 21.51 MiB
In [23]:
%memit sum(i**2 for i in range(10**6))
peak memory: 467.58 MiB, increment: 0.00 MiB

That's because with list you create the whole list and then start adding. With generator comprehension you ask the generator to generate next value and add it to the current sum. There's no need for a container.

It's very useful when you might break at some point:

In [2]:
%%timeit
for i in range(10**6):
    if i == 2:
        break
361 ns ± 24.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [3]:
%%timeit
for i in list(range(10**6)):
    if i == 2:
        break
32.3 ms ± 1.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Yield

In [28]:
def some_generator():
    print("Starting")
    yield 1
    print("Let's come back to where we left off")
    yield 2
    print("Nope. No more yields")
In [29]:
gen = some_generator()
In [30]:
next(gen)
Starting
Out[30]:
1
In [31]:
next(gen)
Let's come back to where we left off
Out[31]:
2
In [32]:
next(gen)
Nope. No more yields
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-32-6e72e47198db> in <module>
----> 1 next(gen)

StopIteration: 
In [33]:
list(some_generator())
Starting
Let's come back to where we left off
Nope. No more yields
Out[33]:
[1, 2]
In [34]:
gen = some_generator()
In [35]:
import inspect
inspect.getgeneratorstate(gen)
Out[35]:
'GEN_CREATED'
In [36]:
next(gen)
inspect.getgeneratorstate(gen)
Starting
Out[36]:
'GEN_SUSPENDED'
In [37]:
list(gen)
inspect.getgeneratorstate(gen)
Let's come back to where we left off
Nope. No more yields
Out[37]:
'GEN_CLOSED'
In [38]:
def primitive_range(start: int, stop: int, step: int = 1):
    current = start
    while current < stop:
        yield current
        current += step


for i in primitive_range(0, 4):
    print(i)
0
1
2
3

The yield statement does 2 things. Freezes the current execution frame (function locals and next instruction to execute) and returns the value

If you come from C or C++ you might find it weird that stack (local variables) are not destructed when returing from function. In Cpython each function call creates new frame object on the heap :D. So python can manage the lifetime of function local variables dynamically.

In [40]:
import inspect
g = primitive_range(0,10)
inspect.getgeneratorlocals(g)
Out[40]:
{'start': 0, 'stop': 10, 'step': 1}
In [41]:
next(g)
Out[41]:
0
In [43]:
inspect.getgeneratorlocals(g)
Out[43]:
{'start': 0, 'stop': 10, 'step': 1, 'current': 0}
In [45]:
next(g)
Out[45]:
1
In [47]:
inspect.getgeneratorlocals(g)
Out[47]:
{'start': 0, 'stop': 10, 'step': 1, 'current': 1}

How does python know where he stopped in generator? The instruction pointer

In [7]:
def simple_gen():
    x = 10
    yield x
    y = "abc"
    yield y
In [8]:
g = simple_gen()
g.gi_frame.f_lasti
Out[8]:
-1
In [9]:
next(g)
g.gi_frame.f_lasti
Out[9]:
6
In [10]:
import dis
dis.disco(g.gi_code, lasti=g.gi_frame.f_lasti)
  2           0 LOAD_CONST               1 (10)
              2 STORE_FAST               0 (x)

  3           4 LOAD_FAST                0 (x)
    -->       6 YIELD_VALUE
              8 POP_TOP

  4          10 LOAD_CONST               2 ('abc')
             12 STORE_FAST               1 (y)

  5          14 LOAD_FAST                1 (y)
             16 YIELD_VALUE
             18 POP_TOP
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE

This was python bytecode. And a topic for a separate CoP.

In [12]:
import dis
print(dis.code_info(simple_gen))
Name:              simple_gen
Filename:          <ipython-input-7-e977d2e2606f>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  2
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, GENERATOR, NOFREE, 0x2000
Constants:
   0: None
   1: 10
   2: 'abc'
Variable names:
   0: x
   1: y

Common generator pitfalls:

In [13]:
def infinite_power_2_gen():
    current = 2
    while True:
        yield current
        current *= 2
In [14]:
powers_of_2 = infinite_power_2_gen()
first_4 = powers_of_2[:4]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-14-20b1b822cb19> in <module>
      1 powers_of_2 = infinite_power_2_gen()
----> 2 first_4 = powers_of_2[:4]

TypeError: 'generator' object is not subscriptable

There are a couple of ways to slice a generator

In [57]:
first_5_elements = []
for i in range(5):
    first_5_elements.append(next(powers_of_2))
first_5_elements
Out[57]:
[2, 4, 8, 16, 32]
In [56]:
first_5_elements = [pair[0] for pair in zip(powers_of_2, range(5))]
first_5_elements
Out[56]:
[64, 128, 256, 512, 1024]
In [59]:
from itertools import islice
first_4_powers_gen = islice(infinite_power_2_gen(), 4)
first_4_powers_gen
Out[59]:
<itertools.islice at 0x7fd10fe13310>

But obviously the islice is the best one.

Generators are one pass. There's no way to reuse a generator object that is already exhausted (that raised StopIteration)

In [61]:
list(first_4_powers_gen)
Out[61]:
[]

With yield statement the FibonacciIterator implemented with iter protocol is much simplier:

In [66]:
def fib_generator():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b
In [67]:
list(islice(fib_generator(), 10))
Out[67]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Real use case - iterative xml parsing

In [6]:
from pathlib import Path
from typing import Generator
import xml.etree.ElementTree as ET


def iter_tags_from_xml_file(path: Path) -> Generator[ET.Element, None, None]:
    """
    Parses xml file incrementally to not bloat the ram on big xml files
    :param path: path to xml_file
    :return: generator iterating over xml tags
    """
    xml_iterator = iter(ET.iterparse(str(path), events=("start", "end")))
    _, root = next(xml_iterator)
    for event, element in xml_iterator:
        if event == "end":
            yield element
        # without clearing the root element the whole tree is still stored in ram, but created incrementally
        root.clear()
In [7]:
! du -hs ~/big.xml
19G	/home/rs/big.xml
In [8]:
tags = iter_tags_from_xml_file(Path("/home/rs/big.xml"))
In [31]:
keybox_tags = (entry for entry in tags if entry.tag == "Keybox")

It's great that up to this point the file hasn't been opened yet :D

In [75]:
%memit keybox_elements = sum(1 for x in keybox_tags)
peak memory: 468.92 MiB, increment: 0.00 MiB

We counted keybox tags in 17GB xml file with just a couple of KB.

Data pipelines

We need some test data. Let's generate it using python

In [16]:
! pip3.8 install names --user
Collecting names
  Downloading https://files.pythonhosted.org/packages/44/4e/f9cb7ef2df0250f4ba3334fbdabaa94f9c88097089763d8e85ada8092f84/names-0.3.0.tar.gz (789kB)
     |████████████████████████████████| 798kB 718kB/s eta 0:00:01
Building wheels for collected packages: names
  Building wheel for names (setup.py) ... done
  Created wheel for names: filename=names-0.3.0-cp38-none-any.whl size=803690 sha256=964a90663fc1b9b1f186031d746fbd29dbc8a4ec96d2ec318b877e375aea310e
  Stored in directory: /home/rs/.cache/pip/wheels/f9/a5/e1/be3e0aaa6fa285575078fa2aafd9959b45bdbc8de8a6803aeb
Successfully built names
Installing collected packages: names
Successfully installed names-0.3.0
In [17]:
import names
import random

with open("student_grades.txt", "w") as file:
    for _ in range(25):
        print(f"{names.get_first_name()} {','.join(map(str,random.choices(range(2,6),k=15)))}", file=file)
In [18]:
!cat student_grades.txt
Krista 2,4,5,2,3,4,2,3,4,4,4,5,2,3,2
Gerald 3,5,5,2,4,2,2,2,5,3,5,4,2,2,5
Gilbert 2,3,4,3,4,4,2,2,4,5,2,3,3,3,3
Paul 4,3,3,4,3,3,3,2,2,2,5,5,5,3,5
Henry 2,5,4,3,5,4,3,4,5,4,4,5,4,4,5
Douglas 4,5,2,2,3,2,5,2,2,2,2,2,2,2,4
Michelle 4,3,4,2,3,4,5,3,2,2,3,4,3,5,4
Jessie 2,3,2,5,5,2,3,2,4,2,3,3,4,5,5
Shirley 4,4,3,2,3,3,3,4,2,5,5,4,4,2,5
Sean 4,5,3,3,4,3,4,5,3,5,5,4,4,4,2
Allen 5,3,2,3,2,3,3,3,4,3,5,5,3,3,4
Kara 3,4,3,5,3,4,4,2,5,3,4,3,4,4,2
Virginia 5,5,4,2,5,2,4,3,4,4,3,4,5,3,5
Marion 3,2,5,2,4,4,2,4,4,2,5,3,5,3,4
Latoya 3,2,2,2,5,3,4,5,2,2,3,5,3,4,4
Percy 4,5,2,5,5,2,4,5,5,5,4,4,5,3,4
Ching 2,3,5,5,4,4,3,3,4,5,5,5,4,3,3
Jane 4,2,3,4,4,3,2,2,3,2,2,3,2,4,2
William 3,5,4,3,5,4,5,5,2,5,3,5,5,3,3
Eleanor 5,2,5,5,2,5,3,2,4,4,2,2,4,5,4
Christopher 3,3,4,3,4,5,2,2,4,3,2,3,3,2,5
Todd 3,2,3,2,2,4,4,5,3,3,3,2,2,3,4
Richard 3,4,5,3,5,2,4,4,3,4,4,5,4,4,5
Michael 4,5,4,3,2,4,3,4,4,5,4,5,5,3,3
Nancy 3,5,5,4,3,2,2,4,5,5,5,5,5,5,2

Find the first student with grades mean less than 3.2. There's no need to find the one with the worst grades. Just find one
Using lists won't scale. In case of a really big file you would run out of ram memory:

In [20]:
from pprint import pprint
from statistics import mean


def get_lines(filename: str):
    lines = []
    with open(filename) as file:
        for line in file:
            lines.append(line)
    return lines


def parse_lines(lines: list):
    students_with_grades = []
    for line in lines:
        student, grades_str = line.split()
        grades = [int(grade) for grade in grades_str.split(",")]
        students_with_grades.append((student, grades))
    return students_with_grades


def get_students_with_means(students: list):
    student_with_means = []
    for student, grades in students:
        student_with_means.append((student, mean(grades)))
    return student_with_means


lines = get_lines("student_grades.txt")
students_with_grades = parse_lines(lines)
student_with_means = get_students_with_means(students_with_grades)
student_with_means
Out[20]:
[('Krista', 3.2666666666666666),
 ('Gerald', 3.4),
 ('Gilbert', 3.1333333333333333),
 ('Paul', 3.466666666666667),
 ('Henry', 4.066666666666666),
 ('Douglas', 2.7333333333333334),
 ('Michelle', 3.4),
 ('Jessie', 3.3333333333333335),
 ('Shirley', 3.533333333333333),
 ('Sean', 3.8666666666666667),
 ('Allen', 3.4),
 ('Kara', 3.533333333333333),
 ('Virginia', 3.8666666666666667),
 ('Marion', 3.466666666666667),
 ('Latoya', 3.2666666666666666),
 ('Percy', 4.133333333333334),
 ('Ching', 3.8666666666666667),
 ('Jane', 2.8),
 ('William', 4),
 ('Eleanor', 3.6),
 ('Christopher', 3.2),
 ('Todd', 3),
 ('Richard', 3.933333333333333),
 ('Michael', 3.8666666666666667),
 ('Nancy', 4)]
In [22]:
next(((student, mean) for student, mean in student_with_means if mean < 3.1))
Out[22]:
('Douglas', 2.7333333333333334)

But this will scale. I've put the corresponding list-generator functions next to each other, so you can how simple was the transition from lists to generators:

In [23]:
from pprint import pprint
from statistics import mean

def get_lines(filename: str):
    lines = []
    with open(filename) as file:
        for line in file:
            lines.append(line)
    return lines


def get_lines_gen(filename: str):
    with open(filename) as file:
        for line in file:
            yield line


def parse_lines_gen(lines):
    for line in lines:
        student, grades_str = line.split()
        grades = [int(grade) for grade in grades_str.split(",")]
        yield (student, grades)
        
def parse_lines(lines):
    students_with_grades = []
    for line in lines:
        student, grades_str = line.split()
        grades = [int(grade) for grade in grades_str.split(",")]
        students_with_grades.append((student, grades))
    return students_with_grades


def get_students_with_means(students):
    student_with_means = []
    for student, grades in students:
        student_with_means.append((student, mean(grades)))
    return student_with_means

def get_students_with_means_gen(students):
    for student, grades in students:
        yield (student, mean(grades))
In [24]:
lines = get_lines_gen("student_grades.txt")
students_with_grades = parse_lines_gen(lines)
student_with_means = get_students_with_means_gen(students_with_grades)

It's great that up to this point no line has been read from file.

In [25]:
list(student_with_means)
Out[25]:
[('Krista', 3.2666666666666666),
 ('Gerald', 3.4),
 ('Gilbert', 3.1333333333333333),
 ('Paul', 3.466666666666667),
 ('Henry', 4.066666666666666),
 ('Douglas', 2.7333333333333334),
 ('Michelle', 3.4),
 ('Jessie', 3.3333333333333335),
 ('Shirley', 3.533333333333333),
 ('Sean', 3.8666666666666667),
 ('Allen', 3.4),
 ('Kara', 3.533333333333333),
 ('Virginia', 3.8666666666666667),
 ('Marion', 3.466666666666667),
 ('Latoya', 3.2666666666666666),
 ('Percy', 4.133333333333334),
 ('Ching', 3.8666666666666667),
 ('Jane', 2.8),
 ('William', 4),
 ('Eleanor', 3.6),
 ('Christopher', 3.2),
 ('Todd', 3),
 ('Richard', 3.933333333333333),
 ('Michael', 3.8666666666666667),
 ('Nancy', 4)]
In [35]:
lines = get_lines_gen("student_grades.txt")
students_with_grades = parse_lines_gen(lines)
student_with_means = get_students_with_means_gen(students_with_grades)
next(((student, mean) for student, mean in student_with_means if mean < 3.1))
Out[35]:
('Douglas', 2.7333333333333334)

With this transition we came from:

  • read all lines, store them in a list
  • parse each lines, store parsed lines in a list
  • count average grades for each student, store the results in a list
  • iterate through list with averages to find the student that should be expelled

to:

  • read first line
  • parse this line
  • count the average
  • check if it's the student that should be expelled
  • repeat until we found the student

The second version is better because:

  • it scales for a really big lists that cannot be loaded to ram memory
  • it can be easilly parallelised as a standard consumer producer problem
  • it is faster because you read and parse just enogh lines to find the correct student

It can also be done with pure generator comprehensions in just a couple of lines. I love python one-liners:

In [36]:
lines = (line for line in open("student_grades.txt"))
splitted_lines = (line.split() for line in lines)
students_with_grades = ((student, [int(grade) for grade in grades_str.split(",")]) for student, grades_str in splitted_lines)
students_with_means = ((student, mean(grades)) for student, grades in students_with_grades)
next(((student, mean) for student, mean in students_with_means if mean < 3.1))
Out[36]:
('Douglas', 2.7333333333333334)

Other generator methods: send, throw, close

In [42]:
def adjustable_counter():
    current = 0
    while True:
        next_val = yield current
        if next_val is not None:
            current = next_val
        current += 1


c = adjustable_counter()
In [43]:
next(c)
Out[43]:
0
In [44]:
next(c)
Out[44]:
1
In [45]:
next(c)
Out[45]:
2

With send() you can send a value to the runnig generator. next() is equivalent to send(None)

In [46]:
c.send(-100) # Spoiler alert - that's how coroutines communitate
Out[46]:
-99
In [47]:
next(c)
Out[47]:
-98

throw() raises the exception inside the generator:

In [48]:
c.throw(RuntimeError("Sorry"))
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-48-4e52024f2adf> in <module>
----> 1 c.throw(RuntimeError("Sorry"))

<ipython-input-42-d112ac940fd0> in adjustable_counter()
      2     current = 0
      3     while True:
----> 4         next_val = yield current
      5         if next_val is not None:
      6             current = next_val

RuntimeError: Sorry

Close raises GeneratorExit inside the generator. This cleans up the generator state.

In [52]:
c.close()
In [53]:
import inspect
inspect.getgeneratorstate(c)
Out[53]:
'GEN_CLOSED'

Context managers - RAII in Python

In [126]:
with open("irrelevant.txt","w") as file:
    file.write("raii")

Is better than:

In [127]:
file = open("irrelevant.txt","w")
try:
    file.write("raii")
finally:
    file.close()
In [128]:
from threading import Lock

lock = Lock()
x = 10
In [129]:
lock.acquire()
x += 1
lock.release()
In [130]:
with lock:
    x += 1
with lock:
    x += 1
In [54]:
%%HTML
<blockquote class="reddit-card" data-card-created="1570178622"><a href="https://www.reddit.com/r/ProgrammerHumor/comments/bfr1xc/i_love_python_but/">I love Python, but...</a> from <a href="http://www.reddit.com/r/ProgrammerHumor">r/ProgrammerHumor</a></blockquote>
<script async src="//embed.redditmedia.com/widgets/platform.js" charset="UTF-8"></script>

How does this work under the hood?

In [133]:
class File:
    def __init__(self, name: str, mode: str = "r"):
        self.name = name
        self.mode = mode
        self.file_handle = None

    def __enter__(self):
        self.file_handle = open(self.name, self.mode)
        return self.file_handle

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("__exit__ called")
        if self.file_handle:
            self.file_handle.close()


with File("irrelevant.txt", "r") as f:
    10 / 0
__exit__ called
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-133-9376f50f68e1> in <module>
     16 
     17 with File("irrelevant.txt", "r") as f:
---> 18     10 / 0

ZeroDivisionError: division by zero

contextlib

@contextmanager - a shortcut for creating contextmanagers
The code up to the first yield statement is executed in __enter__ and the rest is executed in __exit__

In [59]:
from contextlib import contextmanager


@contextmanager
def File(name: str, mode: str = "r"):
    file_handle = None
    try:
        file_handle = open(name, mode)
        yield file_handle
    finally:
        if file_handle:
            print("closing")
            file_handle.close()
In [60]:
with File("irrelevant.txt", "w") as f:
    pass
closing
In [62]:
with File("irrelevant.txt", "w") as f:
    10 / 0
closing
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-62-cda9ffb7c860> in <module>
      1 with File("irrelevant.txt", "w") as f:
----> 2     10 / 0

ZeroDivisionError: division by zero
In [63]:
with File("3.txt", "r") as f:
    10 / 0
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
<ipython-input-63-ed68aac521cf> in <module>
----> 1 with File("3.txt", "r") as f:
      2     10 / 0

/usr/lib/python3.8/contextlib.py in __enter__(self)
    111         del self.args, self.kwds, self.func
    112         try:
--> 113             return next(self.gen)
    114         except StopIteration:
    115             raise RuntimeError("generator didn't yield") from None

<ipython-input-59-63896fde37ed> in File(name, mode)
      6     file_handle = None
      7     try:
----> 8         file_handle = open(name, mode)
      9         yield file_handle
     10     finally:

FileNotFoundError: [Errno 2] No such file or directory: '3.txt'
In [138]:
import sys
import datetime
from typing import Generator
from typing.io import TextIO
from contextlib import contextmanager



@contextmanager
def execution_time_printed(file: TextIO = sys.stdout) -> Generator[None, None, None]:
    start = datetime.datetime.now()
    yield
    print("Execution time:", datetime.datetime.now() - start, file=file)
In [139]:
with execution_time_printed():
    print("inside")
    import time
    time.sleep(0.5)
print("outside")
inside
Execution time: 0:00:00.500736
outside

Reentrant contextmanagers

File cannot be opened after closing:

In [140]:
file = open("irrelevant.txt","w")
with file:
    file.write("a")


with file:
    file.write("a")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-140-9f973b870795> in <module>
      4 
      5 
----> 6 with file:
      7     file.write("a")

ValueError: I/O operation on closed file.

SQL transaction is implemented as contextmanager:

In [14]:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("""
CREATE TABLE numbers (
   number INTEGER
);
""")
with db:
    db.execute("INSERT INTO numbers values (1);")
    db.execute("INSERT INTO numbers values (2);")
list(db.execute("SELECT * from numbers"))
Out[14]:
[(1,), (2,)]
In [17]:
with db:
    db.execute("INSERT INTO numbers values (3);")
    db.execute("INSERT INTO numbers values ('should fail', 2);")
---------------------------------------------------------------------------
OperationalError                          Traceback (most recent call last)
<ipython-input-17-ab390297438e> in <module>
      1 with db:
      2     db.execute("INSERT INTO numbers values (3);")
----> 3     db.execute("INSERT INTO numbers values ('should fail', 2);")

OperationalError: table numbers has 1 columns but 2 values were supplied
In [18]:
list(db.execute("SELECT * from numbers"))
Out[18]:
[(1,), (2,)]

It works, adding number 3 was rolled back

Btw threading pool context manager

In [65]:
from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(max_workers=1) as executor:
    future = executor.submit(pow, 323, 1235)

print(future.result())
733018741971166252529244672995227796776583345783942437386911678051742014907619818389889490523633966891088361470574959346120460571462260749066868322280895817921819178047106351622746089787222709001409723271535998886740900624008120633567081904814632812393533764463134148203902627783449817391855430335563732106041226371560673691183984708116601872233266074247493626304648260263767914583249791984053769482918833516091413131011123944919964273965579371981208614941585953495908535921540210708056885341387737215923345202544222865141850763901074317449693617326298168109535915635940121796253976420394712905525890085230066381155268301872764521970724361150250533240741250911370641578495445037349949847056446112243875919940177685220040160664042867777937760175315580842852772334361081793849764931709880797656009445214260838004008425745329486287217583337196739975624879209997958066918269289529173759400388462627027867023449158801888859657106016910393756105722308386102404609268566550368738720443422703712421937413064228232666730605742388335837146714118055611327514203299723649208298344877573265368313069762781039793226859829488229032564965635731481369752167217585943588854930073123415040389709513212962223638396059885947892393069120539629752913087293860710229309854604802321674065167382714715102476300146693438431833224964213369532024918771644330976993898734654636960222338307689812245659000814812860386873604770823862137671384853381092605916632447158620208279102871362522121737075798746297634021439677681885846906025762209900264981149145400622176046541895913713446104210012678561757945542108470975177238574885971783008091688989094139568186322074509855040235429441987295597788583656003164149522040284092940950997841383270838931250004957560959392148357213125798758355221152004876638131530060401294918574742158403027075892317311713232948774508045444387008377268956511566790988750066274475945057011826156359365664084922898519338475775136584478799585953802786369705215183341960068786541347670144480851661915646211142904738434781594527201521904996219737980968884208250267436396423981779399709186303960293407672554877346877193170897982220120029537710504050457313802321446995592434916524383256286111803049392908220573240027642399185919278901303127877945119916025120410258623536046989159056988455695917457277794657573586903543406892809506842341172324188614146189747989997492566982444418485858555939830994931765546646159933948646331181720254477223093614552419921625948715007606190469859917311824745902574837132432028609681828479562137848761003845549339456254612745501089667544053262976758087180329859844324991679303251520118205924889348811070368534970345655190383142368597970622914787824113699770230464815835988172741718871079971541784210688842581288010501153867757557117911157916834496975757069921359922064002354851413718493414469919988528124300509472377943419402710070938638340816072261896269139282992942967710326722487988387814927252378444344275944579731813013115789048970941051188880593003843626208600970181761638829477861608229607983725051555487136745926844381097959339538810272017727875770008857972396527027073630500507
In [6]:
from concurrent.futures import Executor, ProcessPoolExecutor

with ProcessPoolExecutor(max_workers=8) as executor:
    executor: Executor
    powers = list(executor.map(pow, range(10 ** 4), range(10 ** 4)))
powers[:100]
Out[6]:
[1,
 1,
 4,
 27,
 256,
 3125,
 46656,
 823543,
 16777216,
 387420489,
 10000000000,
 285311670611,
 8916100448256,
 302875106592253,
 11112006825558016,
 437893890380859375,
 18446744073709551616,
 827240261886336764177,
 39346408075296537575424,
 1978419655660313589123979,
 104857600000000000000000000,
 5842587018385982521381124421,
 341427877364219557396646723584,
 20880467999847912034355032910567,
 1333735776850284124449081472843776,
 88817841970012523233890533447265625,
 6156119580207157310796674288400203776,
 443426488243037769948249630619149892803,
 33145523113253374862572728253364605812736,
 2567686153161211134561828214731016126483469,
 205891132094649000000000000000000000000000000,
 17069174130723235958610643029059314756044734431,
 1461501637330902918203684832716283019655932542976,
 129110040087761027839616029934664535539337183380513,
 11756638905368616011414050501310355554617941909569536,
 1102507499354148695951786433413508348166942596435546875,
 106387358923716524807713475752456393740167855629859291136,
 10555134955777783414078330085995832946127396083370199442517,
 1075911801979993982060429252856123779115487368830416064610304,
 112595147462071192539789448988889059930192105219196517009951959,
 12089258196146291747061760000000000000000000000000000000000000000,
 1330877630632711998713399240963346255985889330161650994325137953641,
 150130937545296572356771972164254457814047970568738777235893533016064,
 17343773367030267519903781288812032158308062539012091953077767198995507,
 2050773823560610053645205609172376035486179836520607547294916966189367296,
 248063644451341145494649182395412689744530581492654164321720600128173828125,
 30680346300794274230660433647640397899788170645078853280082659754365153181696,
 3877924263464448622666648186154330754898344901344205917642325627886496385062863,
 500702078263459319174537025249570888246709955377400223021257741084821677152403456,
 66009724686219550843768321818371771650147004059278069406814190436565131829325062449,
 8881784197001252323389053344726562500000000000000000000000000000000000000000000000000,
 1219211305094648479473193481872927834667576992593770717189298225284399541977208231315051,
 170676555274132171974277914691501574771358362295975962674353045737940041855191232907575296,
 24356848165022712132477606520104725518533453128685640844505130879576720609150223301256150373,
 3542118045010639240328481337533320712639808638036812473211109743262552383710557968252383789056,
 524744532468751923546122657597368049278513737089035272057324643668607677682302892208099365234375,
 79164324866862966607842406018063254671922245312646690223362402918484170424104310169552592050323456,
 12158129736671364080886280192352136280305445908985401876990335800107686586023081377754367704855688057,
 1900306380941594479763883944859394903933421733915497351026033862324967197615194912638195921621021097984,
 302182066535432255614734701333399524449282910532282724655138380663835618264136459996754463358299552427939,
 48873677980689257489322752273774603865660850176000000000000000000000000000000000000000000000000000000000000,
 8037480562545943774063961638435258139453693382991023311670379647429452389091570630196571368048020948560431661,
 1343645645152250046583026779322969373035290953763411540290906502671301148502338015157014479136799509522304466944,
 228273036346967044979900512337165522400819024722490933829954793073267717315004135590642802687246850771579138342847,
 39402006196394479212279040100143613805079739270465446667948293404245721771497210611414266254884915640806627990306816,
 6908252164760920851405538694468286082230378724259454186289117297729987129104901877330036086277686990797519683837890625,
 1229984803535237425357460579824952453848609953896821302286319065669207712270213276022808840210306942692366529569453244416,
 222337020242360576812569226538683753874082408437758291741262115823894811650848346334502642370010973465496690788650052277723,
 40794917954274783314474389422963594412010553412954188046665939634971631296545460720786532465498226465248060567545587093733376,
 7596040312163297274222442578208043236112279041839441308045514203595638030283176823539793587591372230230103933110810192201741429,
 1435036016098684342856030763566710717400773837392460666392490000000000000000000000000000000000000000000000000000000000000000000000,
 275006373483461607657434076627252658495183350017755660813753981774508905998081919405140568848353397233796618192645698819765129996471,
 53449019547361999534025300140057538544940601393106611570269540644280818850419033099696863861289188541180498511377339362341642322313216,
 10533405146807286720373659460502060785759379112212598116064998418834781689316645387966435364502141349866164216580595609788325190062013833,
 2104491907585431988618502284342828809117486560121225263528600151456547899286616078556844571139130505063616644582773621942951905668236312576,
 426181657761258833198605424151960757395791315610122269092300199179088043392834051588896184557263865748388820264835885609500110149383544921875,
 87464740776733097769356125936571978049204087241719881761346374524717952404307119962211675102409649648957510056235276523073007403698815894552576,
 18188037387806198379277339915556929647807403283187048631478337739929618787870634227045716719924575689062274471430368865388203540672666042530996797,
 3831589812313461262138726500006414268147534037893115512325908939170687185145438579006950082195309705885134607990418665607337632973770507236843454464,
 817598737071050959409276229318696698168591900537987468276932073768901912096673342793217657607316423968313726492566673678273923566086786121551339775919,
 176684706477838432958329750074291851582748389687561895812160620129261977600000000000000000000000000000000000000000000000000000000000000000000000000000000,
 38662196978715633273404758790074316960214213096178319621856934259807530937321861485192508542873470637501160980081794035970219670238407078788135931371782481,
 8565168191027899133831008848558876386078278675251413891745861716969297101478444754225582357726688645588131450754731704968996267139619369035601073162078388224,
 1920797877785042297826876342398329981366626138903106707239638623062073160162030496354441554187075110650838449453108757445590084411555537438824653742747212640587,
 435973436827325522360279881406914796368935566412408014666801047266959214000936369697318397328752293573138388721289594366953995072735552848220101541587045199118336,
 100140253328453899494506997059845948876248360208192710258703340107188607793155063635811515105559240430619077757390331456723193970237417715907213278114795684814453125,
 23273773687010809805103263055261877739102071580597940409585933109624493442480014587281684425109432546907773222375549181098538730989934437386098275807854764894176935936,
 5472364007515806092890840962213361933646557867359955457554369346343376220574263169290566361924999277451198802156950364045812455566817070274944448633167362192918054601383,
 1301592834942972055182648307417315364538725075960067827915311484722452340966317215805106820959190833309704934346517741237438752456673499160125624414995891111204155079786496,
 313119843606264301926053344163576361349265045995115651405929697601914062309331717222037671868698420619053704956499930323034173850662765737986672484408801585719796136592384409,
 76177348045866392339289727720615561750424801402395196724001565744957137343033038019601000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,
 18739875497044403588343023979942190913870699099585922106152367184893220649019310617359174987694158429118066514085327846177870674743597929299970612055662195817332948573029136642691,
 4661010870363696423905966214003100982132353937802439629342577411201858740087903585402257017449025558046308403555128684298484146339920553893653953988411898447534660818749990933364736,
 1171963849265444210417582587751248824708146148109809710033315342359111701761656602431435296049358716378517967896050409107202745103300944452206991034477139649315017364735008987336482893,
 297864151605271565671522691888487433398201478214104374836863448020189421697406537648052418936130195867966416829477021503670303547569409436317072769246334246265969267698928260777661956096,
 76514281153818492497108910522923939889608448570427803043646059567958108943618778356292728753731576478313833091931623635414286047187179783985819399826089348692903513438068330287933349609375,
 19862704051982797580576125639477612374708322893151441233985491658847582706097318376646920317555554524971459613579567077892532792722158677152071233347563474577287871314398899332488478637162496,
 5210245939718361468048211048414496022534389576033913164940029913016568215580398296261072019231723279851007241838011659882766685337218633992220688288491655299087016195985205218347711578485744737,
 1380878341261486750656911803252309726876604105686729638072729543243701479670593033211008001443536626310535980077544691196522513327846303307992442770355560270350429006522588433404602387992091295744,
 369729637649726772657187905628805440595668764281741102430259972423552570455277523421410650010128232727940978889548326540119429996769494359451621570193644014418071060667659301384999779999159200499899]

contextlib.closing - calls .close() on exit

In [29]:
from contextlib import closing
from urllib.request import urlopen

page = urlopen('http://www.python.org')
try:
    print(next(page))
finally:
    page.close()

page.isclosed()
b'<!doctype html>\n'
Out[29]:
True
In [28]:
from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('http://www.python.org')) as page:
    print(next(page))

page.isclosed()
b'<!doctype html>\n'
Out[28]:
True

contextlib.supress - catches and ignores exception

In [31]:
try:
    raise ValueError()
except ValueError:
    pass
In [30]:
from contextlib import suppress

with suppress(ValueError):
    raise ValueError()

contextlib.redirect_stdout

In [33]:
from contextlib import redirect_stdout
import io

f = io.StringIO()
with redirect_stdout(f):
    help(pow)
f.getvalue()
Out[33]:
'Help on built-in function pow in module builtins:\n\npow(base, exp, mod=None)\n    Equivalent to base**exp with 2 arguments or base**exp % mod with 3 arguments\n    \n    Some types, such as ints, are able to use a more efficient algorithm when\n    invoked using the three argument form.\n\n'

contextlib.ContextDecorator - context manager that can be used as decorator

In [34]:
from contextlib import ContextDecorator

class mycontext(ContextDecorator):
    def __enter__(self):
        print('Starting')
        return self

    def __exit__(self, *exc):
        print('Finishing')
        return False

@mycontext()
def function():
    print('The bit in the middle')
In [35]:
function()
Starting
The bit in the middle
Finishing
In [36]:
with mycontext():
    print('The bit in the middle')
Starting
The bit in the middle
Finishing

contextlib.ExitStack