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.
Try itOne 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.
Try itA 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.
Try itA 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.
Try itLists 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.
Try itA 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.
Try itA 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.
Try itConcurrency
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.
Try itA 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.
Try itA 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.
Try itCalling 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.
Try it