Further

From a method you call by its shape to a coroutine that has not run yet.

Eleven programs past the first page: duck typing, modules, comprehensions, closures, threads, and async. Each one still has a check you answer here.

Shared behavior

A method is there if the object has it

pier.meters() works because Pier defines meters. Another class can define meters too, and a caller can use it without a shared declaration. If the method is missing, the error appears when that line runs.

class Pier:
    def __init__(self, span: int) -> None:
        self.span = span

    def meters(self) -> int:
        return self.span

pier = Pier(12)
print(pier.meters())

What to notice. Nothing registers Pier as a provider of meters ahead of time. The lookup happens on the object, at the call.

What is printed?
What happens if you call meters on an object that has no such method?
Try it

One function, more than one type

wider is written once. It returns whichever argument compares as greater. The two arguments have to support that comparison with each other. The check happens when the line runs.

def wider(left, right):
    if left > right:
        return left
    return right

print(wider(4, 12))

What to notice. 4 and 12 are both ints, and ints can be ordered, so the comparison is legal when it runs. The function does not name the type in its header.

What is printed?
What happens if the call is wider(4, "12")?
Try it

A nested function keeps the names it uses

choose builds north, south, and the function picked. picked uses those two names. choose returns picked, and the call is over. name() still sees north, because the nested function kept the names it needed.

def choose(take_left: bool):
    north = "north"
    south = "south"

    def picked() -> str:
        if take_left:
            return north
        return south

    return picked

name = choose(True)
print(name())

What to notice. north is a local of choose, and it is still available after choose returns. The nested function holds onto the names it reads.

What is printed?
How can name() read north after choose has returned?
Try it

A leading underscore is a courtesy

A module is a file other code can import. Names in that file are visible to the importer. A leading underscore asks people to treat the name as internal. The language still lets you call it.

# field/
# ├── pyproject.toml
# └── notes.py

def _hidden() -> str:
    return "hidden"

def label() -> str:
    return "north"

print(label())

What to notice. On your machine, another file would say import notes and then call notes.label. This page is one file, so the call is direct. _hidden is still a normal function.

Try it opens this one file. A second file, and the import that reaches it, is the shape you use on your own machine.

What is printed?
What happens if this file calls _hidden()?
Try it

Lists and functions

A comprehension builds a new list

The comprehension walks spans, keeps values greater than 5, and builds a new list. spans itself is unchanged. len reports how many values were kept.

spans = [3, 12, 7]
long = [meters for meters in spans if meters > 5]
print(len(long))

What to notice. 3 is left out. 12 and 7 are kept. The new list is long. The source list is still three items.

What is printed?
What is spans after this program runs?
Try it

A function can use a name from outside

coat adds extra to its argument. extra is not a parameter. It is a name in the surrounding block, and coat reads it when coat is called.

extra = 2

def coat(meters: int) -> int:
    return meters + extra

print(coat(10))

What to notice. The call passes 10. extra is already in scope, so the function adds 2 and returns 12.

What is printed?
Where does extra come from inside coat?
Try it

A list can hold different objects

Pier and Cable are different classes. Both have a name method. One list can hold an instance of each, because a list stores references, and every reference is the same size.

class Pier:
    def name(self) -> str:
        return "pier"

class Cable:
    def name(self) -> str:
        return "cable"

parts = [Pier(), Cable()]
print(len(parts))

What to notice. The list does not require the two objects to be the same class. len counts the references, which is two.

What is printed?
Why can Pier and Cable share this list?
Try it

Concurrency

join waits for a thread

Thread starts work on another thread. work appends 12 to a list both threads can see. join waits until that thread finishes, so the print reads the 12.

from threading import Thread

found: list[int] = []

def work() -> None:
    found.append(12)

thread = Thread(target=work)
thread.start()
thread.join()
print(found[0])

What to notice. start returns when the thread has been started, not when work has finished. join is the wait. The list is shared because both threads are in one process.

Python Tutor opens this source in an editor. Its visualizer follows one flow, so a second thread may not step the way it does under CPython on your machine.

What is printed?
How does the print see the 12?
Try it

A queue passes a value across threads

Queue is a line of items. The new thread puts 12. get waits until that item arrives, then returns it. The print writes 12.

from queue import Queue
from threading import Thread

messages: Queue[int] = Queue()

def work() -> None:
    messages.put(12)

thread = Thread(target=work)
thread.start()
meters = messages.get()
thread.join()
print(meters)

What to notice. put places 12 in the queue. get blocks until an item is available. join runs after get, once the worker has finished the put.

Python Tutor opens this source in an editor. Its visualizer follows one flow, so the worker thread may not run there the way it does on your machine.

What is printed?
What does get do if the queue is still empty?
Try it

A lock keeps one update exclusive

Three threads each add one to count. They already share the name, because they share the process. The lock makes each read-and-add finish before another thread starts the same steps. After every join, the count is 3.

from threading import Lock, Thread

count = 0
lock = Lock()

def add() -> None:
    global count
    with lock:
        count += 1

threads = [Thread(target=add) for _ in range(3)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()
print(count)

What to notice. global tells add to rebind the module-level name. with lock: acquires the lock for the block and releases it afterward. The three joins finish before the print.

Python Tutor opens this source in an editor. Its visualizer follows one flow, so these three threads may not interleave there the way they can on your machine.

What is printed?
Why does this counter use a lock?
Try it

Calling an async function builds a coroutine

meters is an async function. Calling it does not run the body and does not produce 12. It returns a coroutine, which is an object that describes the work. This program builds that object, prints a line, and closes it. Nothing awaits it, so the body never runs.

async def meters() -> int:
    return 12

coro = meters()
print("coroutine built")
coro.close()

What to notice. await is how you wait on a coroutine, and it is allowed inside an async function. asyncio.run is a runner that drives a coroutine to the end. This program stops before either of those.

Try it opens this program. It still does not run the body of meters. Running that body takes a runner, and this lesson stops at the coroutine object.

What is printed?
What does the call meters() produce?
Try it